实现单点登出并清除所有token是一个比较常见的需求,Spring Security正是为此而生。下面是实现它的完整攻略:
步骤1:添加依赖
首先,在pom.xml中添加spring-boot-starter-security依赖:
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
该依赖包含了Spring Security和一些常用的依赖。
步骤2:配置security
接下来,我们需要在application.properties或application.yml中添加一些配置,如下所示:
### Session Management ###
server.servlet.session.timeout=1800
security.sessions=ALWAYS
security.max-sessions=1
security.session-registry-alias=sessionRegistry
security.logout-success-url=/
security.logout-url=/logout
logging.level.org.springframework.security=DEBUG
这些配置的含义如下:
server.servlet.session.timeout
:定义会话的过期时间,以秒为单位。在本例中,会话过期时间为30分钟(1800秒)。security.sessions=ALWAYS
:定义始终创建会话。security.max-sessions=1
:允许同时存在的最大会话数量。在本例中,只允许一个会话存在。security.session-registry-alias=sessionRegistry
:定义Session Registry别名。security.logout-success-url=/
:定义退出后跳转到的URL地址。security.logout-url=/logout
:定义退出URL地址。logging.level.org.springframework.security=DEBUG
:定义调试日志级别,方便我们在开发时排查问题。
步骤3:定义全局过滤器
在WebSecurityConfig类中定义一个全局过滤器:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**", "/signup", "/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/login")
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}password").roles("ADMIN");
}
这里我们定义了若干条URL地址和其访问权限的规则,以及登录、失败和退出时的相关处理逻辑。
步骤4:定义SessionRegistry和SessionRegistryImpl
定义SessionRegistry:
@Component
public class SessionRegistryProvider implements ApplicationListener<AbstractAuthenticationEvent> {
private SessionRegistry sessionRegistry = new SessionRegistryImpl();
@Override
public void onApplicationEvent(AbstractAuthenticationEvent event) {
if (event instanceof AbstractAuthenticationFailureEvent) {
return;
}
Authentication authentication = event.getAuthentication();
UserDetails principal = (UserDetails) authentication.getPrincipal();
String sessionId = SessionContextHolder.getSession().getId();
List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false);
for (SessionInformation sessionInformation : sessions) {
if (!sessionId.equals(sessionInformation.getSessionId())) {
sessionInformation.expireNow();
}
}
}
public SessionRegistry getSessionRegistry() {
return sessionRegistry;
}
public void setSessionRegistry(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
}
SessionRegistryImpl:
public class SessionRegistryImpl extends SessionRegistryImpl implements SessionRegistry {
private Map<String, List<SessionInformation>> userSessions = new ConcurrentHashMap<>();
/**
* get all session information
*/
@Override
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
List<SessionInformation> list = new ArrayList<>();
for (List<SessionInformation> sessionInformations : userSessions.values()) {
list.addAll(sessionInformations);
}
return list;
}
/**
* get list of sessions that should be marked as expired
*/
@Override
public List<Object> getAllPrincipals() {
return new ArrayList<>(userSessions.keySet());
}
/**
* get session information for specific user
*/
@Override
public List<SessionInformation> getSessions(Object principal, boolean includeExpiredSessions) {
List<SessionInformation> sessionInformations = userSessions.get(getKey(principal));
if (sessionInformations == null) {
return new ArrayList<>();
}
List<SessionInformation> list = new ArrayList<>();
for (SessionInformation sess : sessionInformations) {
if (includeExpiredSessions || !sess.isExpired()) {
list.add(sess);
}
}
return list;
}
/**
* remove session information from buffer
*/
@Override
public void removeSessionInformation(String sessionId) {
SessionInformation info = getSessionInformation(sessionId);
if (info == null) {
return;
}
List<SessionInformation> usersSessions = userSessions.get(getKey(info.getPrincipal()));
usersSessions.remove(info);
if (usersSessions.size() == 0) {
userSessions.remove(getKey(info.getPrincipal()));
}
}
/**
* updates an existing session with a new lastRequest time
*/
@Override
public void refreshLastRequest(String sessionId) {
SessionInformation info = getSessionInformation(sessionId);
if (info == null) {
return;
}
info.refreshLastRequest();
}
/**
* register new session information
*/
@Override
public void registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (logger.isDebugEnabled()) {
logger.debug("Registering session " + sessionId + ", for principal " + principal);
}
List<SessionInformation> sessionInformations = userSessions.get(getKey(principal));
if (sessionInformations == null) {
sessionInformations = new CopyOnWriteArrayList<>();
userSessions.put(getKey(principal), sessionInformations);
}
sessionInformations.add(new SessionInformation(principal, sessionId, new Date()));
}
/**
* get session information for specific session
*/
public SessionInformation getSessionInformation(String sessionId) {
for (List<SessionInformation> sessionInformations : userSessions.values()) {
for (SessionInformation sess : sessionInformations) {
if (sess.getSessionId().equals(sessionId)) {
return sess;
}
}
}
return null;
}
/**
* generate key for user sessions map
*/
private String getKey(Object principal) {
return principal.toString();
}
}
步骤5:定义SessionContextHolder和JwtUtil
定义SessionContextHolder:
@Component
public class SessionContextHolder implements HttpSessionListener {
private static final AtomicLong sessionIds = new AtomicLong(0);
private static final ThreadLocal<HttpSession> sessionHolder = new ThreadLocal<>();
private static final ThreadLocal<Long> sessionIdsHolder = ThreadLocal.withInitial(sessionIds::incrementAndGet);
@Override
public void sessionCreated(HttpSessionEvent event) {
sessionHolder.set(event.getSession());
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
sessionHolder.remove();
}
public static HttpSession getSession() {
return sessionHolder.get();
}
public static String getSessionId() {
return getSession().getId();
}
public static Long getNewSessionId() {
Long sessionId = sessionIdsHolder.get();
sessionIdsHolder.remove();
return sessionId;
}
}
定义JwtUtil:
@Component
public class JwtUtil {
private static final String SECRET_KEY = "123456";
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private static String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY.getBytes())
.compact();
}
public static Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private static Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private static Date getExpirationDateFromToken(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY.getBytes()).parseClaimsJws(token).getBody().getExpiration();
}
private static String getUsernameFromToken(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY.getBytes()).parseClaimsJws(token).getBody().getSubject();
}
}
步骤6:完成逻辑
最后,我们只需要在退出登录时,清除所有Token,并使已有的会话过期即可。代码如下所示:
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private SessionRegistry sessionRegistry;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
cookie.setMaxAge(0);
cookie.setValue(null);
cookie.setPath("/");
response.addCookie(cookie);
}
}
String sessionId = request.getSession().getId();
SessionInformation sessionInformation = sessionRegistry.getSessionInformation(sessionId);
if (sessionInformation != null) {
sessionInformation.expireNow();
sessionRegistry.removeSessionInformation(sessionId);
}
response.sendRedirect("/login");
}
}
至此,我们已经完成了SpringBoot Security实现单点登出并清除所有Token的全部过程。下面是示例说明。
示例1:
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String token = jwtUtil.generateToken((UserDetails) authentication.getPrincipal());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":200,\"msg\":\"ok\",\"token\":\"" + token + "\"}");
response.setHeader("token", token);
}
}
在登录成功时,我们生成一个Token,并将Token添加到请求头中,以便后续访问时进行认证。
示例2:
@Controller
public class LogoutController {
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
return "/login";
}
}
此处,我们定义一个/logout的URL地址,当用户访问该地址时,系统将清除所有Token,并使已有的会话过期。如果你希望在清除Token时可以自定义逻辑,则可以在CustomLogoutSuccessHandler类中添加相应的处理代码,以满足你的要求。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringBoot Security实现单点登出并清除所有token - Python技术站