Spring Security parse (C) - Personalization achieve certification and RememberMe

Spring Security parse (C) - Personalization achieve certification and RememberMe

  When learning Spring Cloud, met oauth related content authorization service, always scanty, it was decided to first Spring Security, Spring Security Oauth2 and other rights related to certification content, learning theory and design and organize it again. This series of articles is to strengthen the impression that in the process of learning and understanding written, if infringement, please inform.

Project environment:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

A personalized certification

(A) Configuration Log

   In the authorization process and certification process, we are using the Security of a default login page (/ login), so if we want to customize a login page how to achieve it? Actually very simple, we create FormAuthenticationConfig configuration class, and then implement the following settings configure (HttpSecurity http) method:

        http.formLogin()
                //可以设置自定义的登录页面 或者 (登录)接口
                // 注意1: 一般来说设置成(登录)接口后,该接口会配置成无权限即可访问,所以会走匿名filter, 也就意味着不会走认证过程了,所以我们一般不直接设置成接口地址
                // 注意2: 这里配置的 地址一定要配置成无权限访问,否则将出现 一直重定向问题(因为无权限后又会重定向到这里配置的登录页url)
                .loginPage(securityProperties.getLogin().getLoginPage())
                //.loginPage("/loginRequire")
                // 指定验证凭据的URL(默认为 /login) ,
                // 注意1:这里修改后的 url 会意味着  UsernamePasswordAuthenticationFilter 将 验证此处的 url
                // 注意2: 与 loginPage设置的接口地址是有 区别, 一但 loginPage 设置了的是访问接口url,那么此处配置将无任何意义
                // 注意3: 这里设置的 Url 是有默认无权限访问的
                .loginProcessingUrl(securityProperties.getLogin().getLoginUrl())
                //分别设置成功和失败的处理器
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler);

  Last call formAuthenticationConfig.configure (http) to the SpringSecurityConfig of configure (HttpSecurity http) method;

   As you can see, we set by loginPage () login page or interfaces , interface addresses UsernamePasswordAuthenticationFilter to match (by loginProcessingUrl () must be Post ) (read authorization process students should know is that the default / login) . Here the following points should be noted:

  • loginPage () address (url either the interface or the login page) configured here must be configured to access without authority, otherwise there is a problem has been redirected (because there is no authority here, and then will be redirected to the login configuration page url
  • loginPage () in general does not directly set to (login) interface because the interface will be configured to set up no permission to access (of course, set to the login page also no need to configure access), it will go anonymity filter, also means certification process will not go forward, so we generally do not address directly into the interface
  • loginProcessingUrl () url modified here would mean UsernamePasswordAuthenticationFilter the verification url here
  • loginProcessingUrl () Url set here there is no access to the default, and loginPage set of interface addresses are different, but loginPage a set of interfaces is url, then the configuration will have no meaning here
  • successHandler () and failureHandler are set authentication is successful processor and processor authentication failure (if these two processors no impression, it is recommended authorization review process)

(B) the successes and failures of processor configuration

   During the authorization process, we had to add a brief mention of these two processors, the default processor in Security are SavedRequestAwareAuthenticationSuccessHandler and SimpleUrlAuthenticationFailureHandler, this time we customize these two processors, respectively CustomAuthenticationSuccessHandler (extends SavedRequestAwareAuthenticationSuccessHandler) weight write onAuthenticationSuccess () method:

@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private SecurityProperties securityProperties;

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功");
        // 如果设置了loginSuccessUrl,总是跳到设置的地址上
        // 如果没设置,则尝试跳转到登录之前访问的地址上,如果登录前访问地址为空,则跳到网站根路径上
        if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
            requestCache.removeRequest(request, response);
            setAlwaysUseDefaultTargetUrl(true);
            setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }

}

和 CustomAuthenticationFailureHandler( extends SimpleUrlAuthenticationFailureHandler) 重写 onAuthenticationFailure() 方法 :

@Component("customAuthenticationFailureHandler")
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        logger.info("登录失败");
        if (StringUtils.isEmpty(securityProperties.getLogin().getLoginErrorUrl())){

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));

        } else {
            // 跳转设置的登陆失败页面
            redirectStrategy.sendRedirect(request,response,securityProperties.getLogin().getLoginErrorUrl());
        }

    }
}

(C) custom landing page

Not described herein, directly attached to the code:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/loginUp" method="post">  
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>记住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

  Note that the requested address is loginProcessingUrl () configured address

(D) verification test

  In view of the results posted here is not, as long as we understand the results of the process on the line like this you can:
localhost: 8080 -> Click to test to verify Security Access Control ----> Go to our custom login page /loginUp.html , log ----> there is arranged loginSuccessUrl, then jump to loginSuccess.html; otherwise jump to / get_user / test interface to return results. Fully involved in the entire process on the login page to our custom, custom logon success / failure processor.

Two, RememberMe (Remember Me) Functional analysis

(A) RememberMe functions for configuration

首先我们一股脑的将rememberMe配置加上,然后看下现象:

1. Create persistent_logins table for storing information associated with the user's token and:

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

2, add configuration information rememberMe

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 如果token表不存在,使用下面语句可以初始化 persistent_logins(ddl在db目录下) 表;若存在,请注释掉这条语句,否则会报错。
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {

        formAuthenticationConfig.configure(http);
        http.   ....
                .and()
                // 开启 记住我功能,意味着 RememberMeAuthenticationFilter 将会 从Cookie 中获取token信息
                .rememberMe()
                // 设置 tokenRepository ,这里默认使用 jdbcTokenRepositoryImpl,意味着我们将从数据库中读取token所代表的用户信息
                .tokenRepository(persistentTokenRepository())
                // 设置  userDetailsService , 和 认证过程的一样,RememberMe 有专门的 RememberMeAuthenticationProvider ,也就意味着需要 使用UserDetailsService 加载 UserDetails 信息
                .userDetailsService(userDetailsService)
                // 设置 rememberMe 的有效时间,这里通过 配置来设置
                .tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds())
                .and()
                .csrf().disable(); // 关闭csrf 跨站(域)攻击防控
    }

Here explain configuration:

  • rememberMe () function is turned Remember me, it means RememberMeAuthenticationFilter will get token information from the Cookie
  • tokenRepository () token configuration of the acquisition policy, where configured to read from the database
  • userDetailsService () Configuration UserDetaisService (If you are not familiar with the subject, the proposed review of the certification process)
  • Effective time tokenValiditySeconds () is provided rememberMe, here provided by configuring

Another important configuration login page, there must be name = "remember-me", rememberMe remermberMe function is to open by verifying the configuration.

<input name="remember-me" type="checkbox" value="true"/>记住我</td>

  Practical operation result should be: enter the login page -> Login Remember me after check -> After a successful check persistent_logins found to have a data table -> Restart Project -> revisit need to log in to access the page, found accessible without logging in -> delete persistent_logins data table, waiting for a valid token set time expires , then refresh the page jump to find landing page.

(B) RembemberMe implementation source code parsing

   First we see UsernamePasswordAuthenticationFiler (AbstractAuthenticationProcessingFilter) of successfulAuthentication () method inside source:

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        
        // 1 设置 认证成功的Authentication对象到SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(authResult);
        
        // 2 调用 RememberMe 相关service处理
        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
        //3 调用成功处理器
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

In which we find ourselves in this line of code focus: rememberMeServices.loginSuccess (Request, the Response, authResult) , view the source code inside this method:

@Override
    public final void loginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // 这里就在判断用户是否勾选了记住我
        if (!rememberMeRequested(request, parameter)) {
            logger.debug("Remember-me login not requested.");
            return;
        }

        onLoginSuccess(request, response, successfulAuthentication);
    }

By rememberMeRequested () to determine whether checked Remember Me.
onLoginSuccess () method calls to eventually PersistentTokenBasedRememberMeServices the onLoginSuccess () method, a method which put source as follows:

protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // 1 获取账户名
        String username = successfulAuthentication.getName();
        
        // 2 创建  PersistentRememberMeToken 对象
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
            // 3 通过 tokenRepository 存储 persistentRememberMeToken 信息
            tokenRepository.createNewToken(persistentToken);
            // 4 将 persistentRememberMeToken 信息添加到Cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

Source Analysis steps:

  • Obtain account information username
  • Incoming username objects created PersistentRememberMeToken
  • By tokenRepository information stored persistentRememberMeToken
  • Add the information to the Cookie persistentRememberMeToken

  Here tokenRepository is our rememberMe configuration function set. After the above resolution we see rememberServices will create a token information, and stored in the database (because we are configuring a database storage JdbcTokenRepositoryImpl), and the token to add information in the Cookie. Here, we see a number of business processes before RememberMe achieve, how to achieve RememberMe back, I think we probably have a heart end up. Authorized here before direct throw filter class we do not have to mention the process of RememberMeAuthenticationFilter , it is between a filter between the UsernamePasswordAuthenticationFilter and AnonymousAuthenticationFilter, after it is responsible in front of the filter is not successful authentication token to obtain information from the Cookie then through tokenRepository for login user name, and then load the UserDetailsServcie UserDetails information, and then create Authticaton (RememberMeAuthenticationToken) information and then call AuthenticationManager.authenticate () authentication process.

RememberMeAuthenticationFilter

  We look at dofiler method RememberMeAuthenticationFilter source:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //  1 调用 rememberMeServices.autoLogin() 获取Authtication 信息
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);

            if (rememberMeAuth != null) {
                // Attempt authenticaton via AuthenticationManager
                try {
                    // 2 调用 authenticationManager.authenticate() 认证
                    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    
                    ......
                    }

                }
                catch (AuthenticationException authenticationException) {
                .....
            }

            chain.doFilter(request, response);
        }

Our main concern rememberMeServices.autoLogin (request, response) method implementation, view the source code:

@Override
    public final Authentication autoLogin(HttpServletRequest request,
            HttpServletResponse response) {
        // 1 从Cookie 中获取 token 信息
        String rememberMeCookie = extractRememberMeCookie(request);

        if (rememberMeCookie == null) {
            return null;
        }
        
        if (rememberMeCookie.length() == 0) {
            cancelCookie(request, response);
            return null;
        }

        UserDetails user = null;

        try {
            // 2 解析 token信息
            String[] cookieTokens = decodeCookie(rememberMeCookie);
            // 3 通过 token 信息 生成 Uerdetails 信息
            user = processAutoLoginCookie(cookieTokens, request, response);
            userDetailsChecker.check(user);

            logger.debug("Remember-me cookie accepted");
            // 4 通过 UserDetails 信息创建 Authentication 
            return createSuccessfulAuthentication(request, user);
        } 
        .....
    }

Internal implementation steps:

  • Get information from the token and parse Cookie
  • (Achieved processAutoLoginCookie () method) generating a token by parsing UserDetails
  • Authentication is generated by UserDetails (createSuccessfulAuthentication () to create RememberMeAuthenticationToken)

One of the most critical one is processAutoLoginCookie () method is how to generate UserDetails object, we see the source code to achieve this method:

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
            HttpServletRequest request, HttpServletResponse response) {
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
        // 1 通过 tokenRepository 加载数据库token信息
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);

        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());
        // 2 判断 用户传入token和数据中的token是否一致,不一致可能存在安全问题
        if (!presentedToken.equals(token.getTokenValue())) {
            tokenRepository.removeUserTokens(token.getUsername());
            throw new CookieTheftException(
                    messages.getMessage(
                            "PersistentTokenBasedRememberMeServices.cookieStolen",
                            "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        }
        try {
            // 3 更新 token 并添加到Cookie中
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            addCookie(newToken, request, response);
        }
        catch (Exception e) {
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }
        // 4 通过 UserDetailsService().loadUserByUsername() 方法加载UserDetails 信息并返回
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

We look at its internal steps:

  • Load the database token information tokenRepository
  • Determine whether the user of incoming token and data token consistent, inconsistent, there may be security issues
  • Update token and added to the Cookie
  • And back through the load information UserDetails UserDetailsService (). LoadUserByUsername () method

   Here I believe we see the following to understand, why did when enabled rememberMe function to configure tokenRepository and the UserDetailsService.

Here I will not demonstrate the entire process to achieve, and the old rules, the flow chart:

https://img2018.cnblogs.com/blog/1772687/201908/1772687-20190828221251139-652651366.jpg

   This article describes the authentication and personalization RememberMe code can access the code repository security module, github address projects: https://github.com/BUG9/spring-security

         If you are interested in these, welcomed the star, follow, bookmarking, forwarding support!

Guess you like

Origin www.cnblogs.com/bug9/p/11426879.html