美文网首页
Spring Security 4.1.0 初次体验

Spring Security 4.1.0 初次体验

作者: taogan | 来源:发表于2021-06-07 10:10 被阅读0次

最近刚看完了 《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";

}

如有不妥的地方,希望提出来一起学习。

Spring Security 源码地址

相关文章

网友评论

      本文标题:Spring Security 4.1.0 初次体验

      本文链接:https://www.haomeiwen.com/subject/rdakeltx.html