最近刚看完了 《Spring 实战 4》,就想着再巩固一下学的知识,于是就写了一个倾向于Spring Security的例子,在这个例子中有如下功能:
| 目录 | 功能点 | 参考本文配置 |
|---|---|---|
| 1 | 自定义认证器 | DaoAuthenticationProvider |
| 2 | 登录认证 | SecurityConfig.configure() |
| 3 | 登录验证码 | SecurityCodeFilter |
| 4 | 自定义记住功能 | SecurityConfig.configure():rememberMe() |
| 4.1 | 自定义记住功能存储器 | PersistentTokenRepository、JdbcTokenRepositoryImpl |
| 5.1 | 自定义认证成功后的处理器 | CustomAuthenticationSuccessHandler |
| 5.2 | 自定义认证失败后的处理器 | CustomizeAuthenticationFailureHandler |
| 6 | 自定义 session 会话控制认证策略 | CustomConcurrentSessionControlAuthenticationStrategy |
| 7 | 使用 Redis 管理 session | RedisSessionRegistry |
| 8 | 自定义 session 失效处理器 | CustomConcurrentSessionFilter |
| 9 | 退出配置 | SecurityConfig.configure():logout() |
| 10 | 获取在线用户/下线用户 | RedisSessionRegistry |
Spring Security 源码地址
话不多说,直接上代码
首先先定义 Security 配置文件:
#登录参数名
usernameParameter=loginName
#密码参数名
passwordParameter=password
#RememberMeService key
rememberKey=store-key
#记住我cookie名称
rememberMeCookieName=remember-me
#首页
indexUrl=/store/welcome
#登录地址
loginUrl=/store/login
#退出后跳转的地等址
logoutSuccessUrl=/store/welcome
#如果是直接通过访问login请求进行登录成功后,跳转的地址,否则,跳转到前一个请求
defaultTargetUrl=/store/welcome
#拦截请求,多个以逗号分割
antPatterns=/store/user/**
#限制 session 最大会话数量,-1不限制
maximumSessions=-1
/**
* 借助 WebApplicationInitializer 以 Java 的方式来配置 Delegating-FilterProxy
*/
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
Security核心配置类:SecurityConfig
package com.spring.practice.config;
import cn.hutool.core.util.StrUtil;
import com.spring.practice.config.security.PasswordEncoderUtil;
import com.spring.practice.config.security.handler.SessionsExpiredLogoutHandler;
import com.spring.practice.config.security.handler.SessionsExpiredRememberMeLogoutHandler;
import com.spring.practice.config.security.session.CustomConcurrentSessionControlAuthenticationStrategy;
import com.spring.practice.config.security.session.RedisSessionRegistry;
import com.spring.practice.config.security.SysUserService;
import com.spring.practice.config.security.filter.CustomConcurrentSessionFilter;
import com.spring.practice.config.security.filter.SecurityCodeFilter;
import com.spring.practice.config.security.handler.CustomAuthenticationSuccessHandler;
import com.spring.practice.config.security.handler.CustomizeAuthenticationFailureHandler;
import com.spring.practice.util.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.ConcurrentSessionFilter;
@Configuration
//@EnableWebSecurity 注解将会启用 Web 安全功能
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${loginUrl}")
private String loginUrl;
@Value("${usernameParameter}")
private String usernameParameter;
@Value("${passwordParameter}")
private String passwordParameter;
@Value("${rememberKey}")
private String rememberKey;
@Value("${logoutSuccessUrl}")
private String logoutSuccessUrl;
@Value("${defaultTargetUrl}")
private String defaultTargetUrl;
@Value("${antPatterns}")
private String antPatterns;
@Value("${rememberMeCookieName}")
private String rememberMeCookieName;
@Value("${maximumSessions}")
private int maximumSessions;
@Autowired
private SysUserService userService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedisService redisService;
/**
* 拦截请求
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.addFilterBefore(this.securityCodeFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(this.customConcurrentSessionFilter(), ConcurrentSessionFilter.class)
// 登录认证配置
.formLogin()
.usernameParameter(usernameParameter)
.passwordParameter(passwordParameter)
.loginPage(loginUrl)
.successHandler(this.successHandler())
.failureHandler(this.failureHandler())
.and()
// 记住功能
.rememberMe()
.rememberMeServices(this.rememberMeServices())
.rememberMeCookieName(rememberMeCookieName)
// 自定义rememberMeServices时,如果不指定key,将会无法使用此功能
.key(rememberKey)
.and()
// 退出配置
.logout()
.logoutSuccessUrl(logoutSuccessUrl)
.addLogoutHandler(this.sessionsExpiredLogoutHandler())
.and()
// 拦截请求配置
.authorizeRequests()
.antMatchers(StrUtil.splitToArray(antPatterns, ',')).authenticated()
.anyRequest().permitAll()
.and()
.sessionManagement()
.sessionAuthenticationStrategy(this.authenticationStrategy())
.maximumSessions(maximumSessions)
.sessionRegistry(this.sessionRegistry());
}
/**
* 用户认证
*
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.daoAuthenticationProvider());
}
/**
* 自定义认证器
*
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 防止出现spring忽略UserNotFoundExceptions
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(PasswordEncoderUtil.getPasswordEncoder());
provider.setUserDetailsService(userService);
return provider;
}
/***
* 验证码过滤器
* @return
*/
@Bean
public SecurityCodeFilter securityCodeFilter() {
SecurityCodeFilter securityCodeFilter = new SecurityCodeFilter();
securityCodeFilter.setLoginUrl(loginUrl);
securityCodeFilter.setFailureHandler(this.failureHandler());
return securityCodeFilter;
}
/**
* 自定义记住我服务
*
* @return
*/
@Bean
public RememberMeServices rememberMeServices() {
PersistentTokenBasedRememberMeServices rememberMeServices =
new PersistentTokenBasedRememberMeServices(rememberKey, userService, this.tokenRepository());
// 半小时
rememberMeServices.setTokenValiditySeconds(60 * 30);
return rememberMeServices;
}
/**
* 自定义记住我服务存储器
*/
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setJdbcTemplate(jdbcTemplate);
return tokenRepository;
}
/**
* 认证成功处理器
*
* @return
*/
@Bean
public CustomAuthenticationSuccessHandler successHandler() {
CustomAuthenticationSuccessHandler successHandler = new CustomAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl(defaultTargetUrl);
return successHandler;
}
/**
* 认证失败处理器
*
* @return
*/
@Bean
public AuthenticationFailureHandler failureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
/**
* session 注册
*
* @return
*/
@Bean
public RedisSessionRegistry sessionRegistry() {
return new RedisSessionRegistry(redisService);
}
/**
* 自定义 session失效处理器
*/
@Bean
public CustomConcurrentSessionFilter customConcurrentSessionFilter() {
CustomConcurrentSessionFilter concurrentSessionFilter
= new CustomConcurrentSessionFilter(this.sessionRegistry());
concurrentSessionFilter.addLogoutHandler(this.cookieNameLogoutHandler());
concurrentSessionFilter.addLogoutHandler(this.sessionsExpiredLogoutHandler());
return concurrentSessionFilter;
}
/**
* 当session过期后,删除浏览器rememberMeCookie,防止二次登录
*/
@Bean
public SessionsExpiredRememberMeLogoutHandler cookieNameLogoutHandler() {
SessionsExpiredRememberMeLogoutHandler logoutHandler =
new SessionsExpiredRememberMeLogoutHandler(rememberMeCookieName);
logoutHandler.setTokenRepository(this.tokenRepository());
return logoutHandler;
}
/**
* 当session过期后,删除持久化session信息
*/
@Bean
public SessionsExpiredLogoutHandler sessionsExpiredLogoutHandler() {
return new SessionsExpiredLogoutHandler(this.sessionRegistry);
}
/**
* 自定义 session 会话控制认证策略
*/
@Bean
public CustomConcurrentSessionControlAuthenticationStrategy authenticationStrategy() {
CustomConcurrentSessionControlAuthenticationStrategy authenticationStrategy =
new CustomConcurrentSessionControlAuthenticationStrategy(this.sessionRegistry());
authenticationStrategy.setMaximumSessions(maximumSessions);
return authenticationStrategy;
}
}
SecurityConfig 看起来比较多,主要是根据自己的需求自定义了很多类,下面我们来查看每个配置类的代码:
1、自定义认证器:DaoAuthenticationProvider
/**
* 自定义认证器
*
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 防止出现spring忽略UserNotFoundExceptions
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(PasswordEncoderUtil.getPasswordEncoder());
provider.setUserDetailsService(userService);
return provider;
}
2、登录配置
登录配置相对比较简单
// 登录配置部分代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
........
// 登录认证配置
.formLogin()
.usernameParameter(usernameParameter)
.passwordParameter(passwordParameter)
.loginPage(loginUrl)
........
}
调用formLogin()可以配置登录页面相关的内容,通过loginPage可以自定义的登录请求路径,usernameParameter和passwordParameter则是表单输入域中用户名和密码的参数名,loginPage默认值为/login,usernameParameter默认值为username,passwordParameter默认值为password。
3、登录验证码:SecurityCodeFilter
/**
* 验证码拦截器
*/
public class SecurityCodeFilter extends OncePerRequestFilter {
private String loginUrl;
private AuthenticationFailureHandler failureHandler;
public SecurityCodeFilter() {
}
public String getLoginUrl() {
return loginUrl;
}
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
public AuthenticationFailureHandler getFailureHandler() {
return failureHandler;
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if ("post".equalsIgnoreCase(httpServletRequest.getMethod())
&& loginUrl.equalsIgnoreCase(httpServletRequest.getServletPath())) {
try {
this.verifySecurityCode(httpServletRequest);
} catch (SecurityCodeException e) {
failureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void verifySecurityCode(HttpServletRequest httpServletRequest) {
String securityCode = (String) httpServletRequest.getSession().getAttribute(SpringSecurityConstants.SECURITY_SESSION_CODE_KEY);
String code = httpServletRequest.getParameter(SpringSecurityConstants.SECURITY_PARAM_CODE_KEY);
if (StrUtil.isEmpty(securityCode)) {
throw new SecurityCodeException("验证码异常");
}
if (StrUtil.isEmpty(code) || !StrUtil.equalsIgnoreCase(securityCode, code)) {
throw new SecurityCodeException("验证码错误");
}
httpServletRequest.getSession().removeAttribute(SpringSecurityConstants.SECURITY_SESSION_CODE_KEY);
}
}
4、自定义记住功能
// 自定义记住功能部分代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.......
// 记住功能
.rememberMe()
.rememberMeServices(this.rememberMeServices())
.rememberMeCookieName(rememberMeCookieName)
// 自定义rememberMeServices时,如果不指定相同的key,记住我功能将失效
.key(rememberKey)
.......
}
4.1、自定义记住功能存储器
/**
* 自定义记住我Service,持久化token
*
* @return
*/
@Bean
public RememberMeServices rememberMeServices() {
PersistentTokenBasedRememberMeServices rememberMeServices =
new PersistentTokenBasedRememberMeServices(rememberKey, userService, this.tokenRepository());
// 半小时
rememberMeServices.setTokenValiditySeconds(60 * 30);
return rememberMeServices;
}
/**
* 自定义记住我服务存储器
*/
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setJdbcTemplate(jdbcTemplate);
return tokenRepository;
}
5、自定义认证处理器
// 登录配置部分代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
........
// 登录认证配置
.formLogin()
......
.successHandler(this.successHandler())
.failureHandler(this.failureHandler())
........
}
5.1、认证成功处理器
/**
* 自定义认证成功后的处理
*/
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void setDefaultTargetUrl(String defaultTargetUrl) {
super.setDefaultTargetUrl(defaultTargetUrl);
}
public String getDefaultUrl() {
return super.getDefaultTargetUrl();
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (this.isAjax(request)) {
String redirectUrl;
// 获取登录前的请求路径
DefaultSavedRequest savedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if (savedRequest == null) {
redirectUrl = this.determineTargetUrl(request, response);
} else {
redirectUrl = savedRequest.getServletPath();
}
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "登录成功");
result.put("redirect", redirectUrl);
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(result));
writer.flush();
writer.close();
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
public boolean isAjax(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
}
5.2、认证失败处理器
/**
* 自定义认证失败后的处理
*/
public class CustomizeAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (this.isAjax(request)) {
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
if (exception instanceof UsernameNotFoundException) {
result.put("message", exception.getMessage());
} else if (exception instanceof LockedException) {
result.put("message", "帐号已锁定");
} else if (exception instanceof CredentialsExpiredException) {
result.put("message", "密码过期");
} else if (exception instanceof AccountExpiredException) {
result.put("message", "账户过期");
} else if (exception instanceof DisabledException) {
result.put("message", "账户被禁用");
} else if (exception instanceof BadCredentialsException) {
result.put("message", "密码输入错误");
} else {
result.put("message", exception.getMessage());
}
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
} else {
super.onAuthenticationFailure(request, response, exception);
}
}
public boolean isAjax(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
}
6、自定义 session 会话控制认证策略
public class CustomConcurrentSessionControlAuthenticationStrategy extends ConcurrentSessionControlAuthenticationStrategy {
private RedisSessionRegistry sessionRegistry;
private boolean exceptionIfMaximumExceeded = false;
private int maximumSessions = 1;
public CustomConcurrentSessionControlAuthenticationStrategy(RedisSessionRegistry sessionRegistry) {
super(sessionRegistry);
this.sessionRegistry = sessionRegistry;
}
@Override
public void setMaximumSessions(int maximumSessions) {
this.maximumSessions = maximumSessions;
}
@Override
public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
}
@Override
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException {
if (!this.exceptionIfMaximumExceeded && sessions != null) {
SessionInformation leastRecentlyUsed = null;
Iterator var5 = sessions.iterator();
while (true) {
SessionInformation session;
do {
if (!var5.hasNext()) {
// 设置 session 过期
this.expireNow(leastRecentlyUsed.getSessionId());
return;
}
session = (SessionInformation) var5.next();
} while (leastRecentlyUsed != null && !session.getLastRequest().before(leastRecentlyUsed.getLastRequest()));
leastRecentlyUsed = session;
}
} else {
throw new SessionAuthenticationException(this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));
}
}
private void expireNow(String sessionId) {
sessionRegistry.expireNow(sessionId);
}
@Override
protected int getMaximumSessionsForThisUser(Authentication authentication) {
return this.maximumSessions;
}
}
7、使用 Redis 管理 session
public class RedisSessionRegistry implements SessionRegistry, ApplicationListener<SessionDestroyedEvent>, InitializingBean {
// 可自定义 session 前缀
private String sessionPrefixKey = SpringSecurityConstants.SECURITY_REDIS_PREFIX;
private String sessionIdsKey;
private String principalsKey;
private RedisService redisService;
public RedisSessionRegistry(RedisService redisService) {
this.redisService = redisService;
}
public void setSessionPrefixKey(String sessionPrefixKey) {
this.sessionPrefixKey = sessionPrefixKey;
}
public String getSessionPrefixKey() {
return sessionPrefixKey;
}
private void setSessionKey() {
this.sessionIdsKey = this.sessionPrefixKey + ":sessionIds";
this.principalsKey = this.sessionPrefixKey + ":principals";
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.hasText(sessionPrefixKey, "sessionPrefixKey required");
Assert.notNull(redisService, "RedisService required");
this.setSessionKey();
}
/**
* 获取所有用户凭证
*
* @return
*/
@Override
public List<Object> getAllPrincipals() {
return new ArrayList<>(this.redisService.keys(this.principalsKey));
}
/**
* 根据用户凭证获取 session 信息
*
* @param principal 用户凭证
* @param includeExpiredSessions 是否包含过期用户
* @return
*/
@Override
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
String username;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else {
username = principal.toString();
}
Set<String> sessionsUsedByPrincipal = this.redisService.hashGet(this.principalsKey, username);
if (sessionsUsedByPrincipal == null || sessionsUsedByPrincipal.isEmpty()) {
return Collections.emptyList();
} else {
List<SessionInformation> list = new ArrayList<>(sessionsUsedByPrincipal.size());
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = this.getSessionInformation(sessionId);
if (sessionInformation != null) {
// 如果需要包含过期用户信息,直接添加全部数据
if (includeExpiredSessions) {
list.add(sessionInformation);
} else if (!sessionInformation.isExpired()) {
list.add(sessionInformation);
}
}
}
return list;
}
}
/**
* 根据 sessionId 获取用户凭证
*
* @param sessionId session Id
* @return
*/
@Override
public SessionInformation getSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
return this.redisService.hashGet(this.sessionIdsKey, sessionId);
}
/**
* 根据 session id 刷新最后一次请求时间
*
* @param sessionId session id
*/
@Override
public void refreshLastRequest(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
this.redisService.put(this.sessionIdsKey, sessionId, info);
}
/**
* 根据 session id 设置过期标识
*
* @param sessionId session id
*/
public void expireNow(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
info.expireNow();
}
this.redisService.put(this.sessionIdsKey, sessionId, info);
}
/**
* 注册新的session
*
* @param sessionId session ID
* @param principal 用户凭证
*/
@Override
// @Transactional
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 (this.getSessionInformation(sessionId) != null) {
this.removeSessionInformation(sessionId);
}
this.redisService.put(this.sessionIdsKey, sessionId, new CustomSessionInformation(principal, sessionId, new Date()));
String username = ((UserDetails) principal).getUsername();
Set<String> sessionsUsedByPrincipal = this.redisService.hashGet(this.principalsKey, username);
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
}
sessionsUsedByPrincipal.add(sessionId);
this.redisService.put(this.principalsKey, username, sessionsUsedByPrincipal);
}
/**
* 删除 session 信息
*
* @param sessionId session ID
*/
@Override
// @Transactional
public void removeSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
this.redisService.hashDel(this.sessionIdsKey, sessionId);
String username = ((UserDetails) info.getPrincipal()).getUsername();
Set<String> sessionsUsedByPrincipal = this.redisService.hashGet(this.principalsKey, username);
if (sessionsUsedByPrincipal != null) {
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
this.redisService.hashDel(this.principalsKey, username);
} else {
this.redisService.put(this.principalsKey, username, sessionsUsedByPrincipal);
}
}
}
}
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
String sessionId = event.getId();
this.removeSessionInformation(sessionId);
}
}
8、自定义 session 失效处理器
public class CustomConcurrentSessionFilter extends ConcurrentSessionFilter {
private RequestMatcher requestMatcher;
private SessionRegistry sessionRegistry;
private List<LogoutHandler> handlers = new ArrayList<>();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void afterPropertiesSet() {
handlers.add(new SecurityContextLogoutHandler());
}
public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry) {
super(sessionRegistry);
this.sessionRegistry = sessionRegistry;
}
public void setRequestMatcher(RequestMatcher requestMatcher) {
this.requestMatcher = requestMatcher;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
if (session != null) {
SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
if (info != null) {
if (info.isExpired()) {
this.doLogout(request, response);
String targetUrl = super.determineExpiredUrl(request, info);
if (targetUrl != null) {
this.redirectStrategy.sendRedirect(request, response, targetUrl);
return;
}
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write("登录失效");
writer.flush();
writer.close();
return;
}
}
}
chain.doFilter(request, response);
}
private boolean matches(HttpServletRequest request) {
if (requestMatcher == null) {
return true;
}
return requestMatcher.matches(request);
}
private void doLogout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
for (int var6 = 0; var6 < handlers.size(); ++var6) {
LogoutHandler handler = handlers.get(var6);
handler.logout(request, response, auth);
}
}
public void addLogoutHandler(LogoutHandler logoutHandler) {
Assert.notNull(logoutHandler, "LogoutHandler required");
handlers.add(logoutHandler);
}
}
/**
* session过期后,清除记住我cookie和持久数据
*/
public class SessionsExpiredRememberMeLogoutHandler implements LogoutHandler, InitializingBean {
private String cookieName;
private PersistentTokenRepository tokenRepository;
public SessionsExpiredRememberMeLogoutHandler() {
}
public SessionsExpiredRememberMeLogoutHandler(String cookieName) {
this.cookieName = cookieName;
}
public void setTokenRepository(PersistentTokenRepository tokenRepository) {
this.tokenRepository = tokenRepository;
}
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.hasLength(this.cookieName, "cookieName cannot be empty or null");
Assert.notNull(this.tokenRepository, "tokenRepository required");
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
this.cleanCookie(request, response);
this.removeUserTokens(authentication);
}
private void cleanCookie(HttpServletRequest request, HttpServletResponse response) {
Cookie cookie = new Cookie(cookieName, null);
String cookiePath = request.getContextPath();
if (!StringUtils.hasLength(cookiePath)) {
cookiePath = "/";
}
cookie.setPath(cookiePath);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
private void removeUserTokens(Authentication authentication) {
if (authentication != null) {
this.tokenRepository.removeUserTokens(authentication.getName());
}
}
}
/**
* 退出时删除session信息
*/
public class SessionsExpiredLogoutHandler implements LogoutHandler {
private SessionRegistry sessionRegistry;
public SessionsExpiredLogoutHandler(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
HttpSession session = httpServletRequest.getSession(false);
if (session != null) {
sessionRegistry.removeSessionInformation(session.getId());
}
}
}
9、退出配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.// 退出配置
.logout()
.logoutSuccessUrl(logoutSuccessUrl)
.addLogoutHandler(this.sessionsExpiredLogoutHandler())
}
10、获取在线用户/下线用户
@Controller
public class OnlineOfflineController {
@Autowired
RedisSessionRegistry sessionRegistry;
//获取在线用户
@GetMapping("/store/online")
public String onlineInformations(Model model) {
List<OnlineInformation> informationList = new ArrayList<>();
for (Object principal : sessionRegistry.getAllPrincipals()) {
List<SessionInformation> sessionInformation = sessionRegistry.getAllSessions(principal, false);
for (SessionInformation information : sessionInformation) {
OnlineInformation onlineInformation = new OnlineInformation((LoginUser) information.getPrincipal(),
information.getSessionId(),
information.getLastRequest());
informationList.add(onlineInformation);
}
}
model.addAttribute("informationList", informationList);
return "store/online";
}
// 下线用户
@GetMapping("/store/offline/{sessionid}")
public String offline(@PathVariable String sessionid) {
sessionRegistry.expireNow(sessionid);
return "redirect:/store/online";
}
}
补充:
1、认证Service
@Component
public class SysUserService implements UserDetailsService {
@Autowired
private SysUserRepository userRepository;
@Autowired
private SysRoleRepository sysRoleRepository;
@Override
public UserDetails loadUserByUsername(String loginName) {
SysUser sysUser = userRepository.findUserByLoginName(loginName);
if (sysUser == null) {
throw new UsernameNotFoundException("用户" + loginName + "不存在");
} else if ("1".equals(sysUser.getStatus())) {
throw new DisabledException("帐号" + loginName + "已停用");
}
Set<String> roleKeys = sysRoleRepository.findUserRole(sysUser.getUserId()).stream()
.map(SysRole::getRoleKey)
.collect(Collectors.toSet());
List<GrantedAuthority> authorities = new ArrayList<>();
for (String s : roleKeys) {
authorities.add(new CustomGrantedAuthority("ROLE_" + s));
}
return new LoginUser(sysUser, authorities);
}
}
2、登录用户实体
/**
* 自定义的目的主要是redis序列化时需要默认构造,UserDetails 默认实现没有默认构造
*/
public class LoginUser implements UserDetails, CredentialsContainer {
private String password;
private String username;
private Set<GrantedAuthority> authorities;
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
private SysUser sysUser;
private Set<String> permissions;
public LoginUser() {
}
public LoginUser(String username, String password) {
this.username = username;
this.password = password;
}
public LoginUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
this(sysUser, authorities, null);
}
public LoginUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities, Set<String> permissions) {
// LoginName 在这里是唯一标识
this.username = String.valueOf(sysUser.getLoginName());
this.password = sysUser.getPassword();
this.authorities = new TreeSet<>(authorities);
this.sysUser = sysUser;
this.permissions = permissions;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
@Override
public boolean equals(Object obj) {
return obj instanceof LoginUser && this.username.equals(((LoginUser) obj).username);
}
@Override
public int hashCode() {
return this.username.hashCode();
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
/**
* 删除密码
*/
@Override
public void eraseCredentials() {
this.password = null;
this.sysUser.setPassword(null);
}
}
3、用户授权
/**
* 自定义的目的主要是redis序列化时需要默认构造,GrantedAuthority 默认实现没有默认构造
*/
public class CustomGrantedAuthority implements GrantedAuthority, Comparable<CustomGrantedAuthority> {
private String authority;
public CustomGrantedAuthority() {
}
public CustomGrantedAuthority(String authority) {
Assert.hasText(authority, "A granted authority textual representation is required");
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else {
return obj instanceof CustomGrantedAuthority && this.authority.equals(((CustomGrantedAuthority) obj).authority);
}
}
@Override
public int hashCode() {
return this.authority.hashCode();
}
@Override
public String toString() {
return this.authority;
}
@Override
public int compareTo(CustomGrantedAuthority o) {
return this.authority.compareTo(o.authority);
}
}
4、Session 实体
/**
* 自定义的目的主要是redis序列化时需要默认构造,SessionInformation没有默认构造
*/
public class CustomSessionInformation extends SessionInformation {
private static final Object PRINCIPAL = new Object();
private static final Date LAST_REQUEST = new Date();
private Date lastRequest;
private Object principal;
private String sessionId;
private boolean expired = false;
public CustomSessionInformation() {
super(PRINCIPAL, "-1", LAST_REQUEST);
}
public CustomSessionInformation(Object principal, String sessionId, Date lastRequest) {
super(principal, sessionId, lastRequest);
this.principal = principal;
this.sessionId = sessionId;
this.lastRequest = lastRequest;
}
public void expireNow() {
this.expired = true;
}
public Date getLastRequest() {
return this.lastRequest;
}
public Object getPrincipal() {
return this.principal;
}
public String getSessionId() {
return this.sessionId;
}
public boolean isExpired() {
return this.expired;
}
public void refreshLastRequest() {
this.lastRequest = new Date();
}
}
5、密码匹配器
public class PasswordEncoderUtil {
private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(10);
public static PasswordEncoder getPasswordEncoder() {
return PASSWORD_ENCODER;
}
public static String encode(CharSequence rawPassword) {
return PASSWORD_ENCODER.encode(rawPassword);
}
public static boolean matches(CharSequence rawPassword, String encoderPassword) {
return PASSWORD_ENCODER.matches(rawPassword, encoderPassword);
}
}
6、Security 工具类
public class SecurityUtils {
public static LoginUser getLoginUser() {
return (LoginUser) SecurityUtils.authentication().getPrincipal();
}
public static SysUser getSysUser() {
return SecurityUtils.getLoginUser().getSysUser();
}
public static Authentication authentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
public static boolean isAuthenticated() {
return SecurityUtils.authentication().isAuthenticated();
}
}
常量
public class SpringSecurityConstants {
/**
* 会话验证码 key
*/
public static final String SECURITY_SESSION_CODE_KEY = "security-code";
/**
* 参数验证码 key
*/
public static final String SECURITY_PARAM_CODE_KEY = "code";
/**
* session 持久化 redis key 前缀标识
*/
public static final String SECURITY_REDIS_PREFIX = "security-session";
}
如有不妥的地方,希望提出来一起学习。









网友评论