三、Spring Security登錄、Session及退出配置

一、登錄配置

對(duì)于表單登錄,能配置登錄成功和失敗的跳轉(zhuǎn)和重定向,Spring Security通過(guò)配置可以實(shí)現(xiàn)自定義跳轉(zhuǎn)、重定向,以及用戶未登錄和登錄用戶無(wú)權(quán)限的處理。

1.1、URL配置

1.1.1、添加依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1.1.2、自定義登錄頁(yè)面

resources/templates下編寫簡(jiǎn)單test-login.html登錄頁(yè)面(參考官方文檔),內(nèi)容如下:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>登錄頁(yè)面</title>
</head>
<body>
    <div th:if="${param.error}">
        <p>用戶名或密碼無(wú)效</p>
    </div>
    <form th:action="@{/my-login}" method="post">
        <div><label> 用戶名 : <input type="text" name="username"/> </label></div>
        <div><label> 密碼: <input type="password" name="password"/> </label></div>
        <button type="submit" class="btn">登錄</button>
    </form>
</body>
</html>

用戶名和密碼名稱默認(rèn)是usernamepassword

創(chuàng)建登錄頁(yè)面映射Controller

@Controller  // 這里使用@Controller,跳轉(zhuǎn)動(dòng)態(tài)頁(yè)面
public class PageController {
    @GetMapping("/user-login")
    public String myLoginPage(){
        return "test-login.html";
    }
}

1.1.3、WebSecurityConfig配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    // 自定義頁(yè)面的路徑不用驗(yàn)證
                    .antMatchers(HttpMethod.GET, "/user-login").permitAll()
                    .anyRequest().authenticated() 
                .and()
                    .formLogin()
                    // 設(shè)置自定義登錄的頁(yè)面
                    .loginPage("/user-login")
                    // 登錄頁(yè)表單提交的 action(th:action="@{/my-login}") URL
                    .loginProcessingUrl("/my-login");   
                    // post請(qǐng)求默認(rèn)需要csrf驗(yàn)證, 這里使用Thymeleaf模板引擎,表單默認(rèn)發(fā)送csrf,可不用關(guān)閉
                    //.and()
                    //.csrf().disable(); 
    }
}

啟動(dòng)程序后,訪問(wèn)localhost:8080/hello,會(huì)跳轉(zhuǎn)到自定義登錄頁(yè)面登錄成功,在F12可以看到自動(dòng)發(fā)送csrf

圖片

其他的登錄成功和登錄失敗參考上面,配置如下:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    // 自定義頁(yè)面的路徑不用驗(yàn)證
                    .antMatchers(HttpMethod.GET, "/user-login").permitAll()
                    // 失敗跳轉(zhuǎn)不用驗(yàn)證
                    .antMatchers(HttpMethod.GET, "/user-fail").permitAll()
                    .anyRequest().authenticated() 
                .and()
                    .formLogin()
                    // 設(shè)置自定義登錄的頁(yè)面
                    .loginPage("/user-login")
                    // 登錄頁(yè)表單提交的 action(th:action="@{/my-login}") URL
                    .loginProcessingUrl("/my-login");   
                   // .usernameParameter("username") // 默認(rèn)就是 username
                  // .passwordParameter("password") // 默認(rèn)就是 password
                    /** 
                     *  登錄成功跳轉(zhuǎn):
                     *  登錄成功,如果是直接從登錄頁(yè)面登錄,會(huì)跳轉(zhuǎn)到該URL;
                     *  如果是從其他頁(yè)面跳轉(zhuǎn)到登錄頁(yè)面,登錄后會(huì)跳轉(zhuǎn)到原來(lái)頁(yè)面。
                     *  可設(shè)置true來(lái)任何時(shí)候到跳轉(zhuǎn) .defaultSuccessUrl("/hello2", true);
                     */
                    .defaultSuccessUrl("/hello2");
                    /**
                     *  登錄成功重定向(和上面二選一)
                     */
                    .successForwardUrl("/hello3")
                    /**
                     *  登錄失敗跳轉(zhuǎn),指定的路徑要能匿名訪問(wèn)
                     */
                    .failureUrl("/login-fail")
                      /**
                       *  登錄失敗重定向(和上面二選一)
                       */
                    .failureForwardUrl("/login-fail");
                    // post請(qǐng)求需要csrf驗(yàn)證, 這里使用Thymeleaf模板引擎,表單默認(rèn)發(fā)送csrf,可不用關(guān)閉
                    //.and()
                    //.csrf().disable(); 
    }
}

1.2、登錄處理器

上面使用URL進(jìn)行的配置,都是通過(guò)Security默認(rèn)提供的處理器處理的,一般多用于前后端不分離。

Spring SecurityAuthenticationManager用來(lái)處理身份認(rèn)證的請(qǐng)求,處理的結(jié)果分兩種:

  • 認(rèn)證成功:結(jié)果由AuthenticationSuccessHandler處理
  • 認(rèn)證失?。航Y(jié)果由AuthenticationFailureHandler處理。

Spring Security提供了多個(gè)實(shí)現(xiàn)于AuthenticationSuccessHandler接口和CustomAuthenticationFailHandler接口的子類,想自定義處理器,可以實(shí)現(xiàn)接口,或繼承接口的實(shí)現(xiàn)類來(lái)重寫。

1.2.1、自定義AuthenticationSuccessHandler

AuthenticationSuccessHandler是身份驗(yàn)證成功處理器的接口,其下有多個(gè)子類:

  • SavedRequestAwareAuthenticationSuccessHandler:默認(rèn)的成功處理器,默認(rèn)驗(yàn)證成功后,跳轉(zhuǎn)到原路徑。也可通過(guò)defaultSuccessUrl()配置。
  • SimpleUrlAuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandler的父類,只有指定defaultSuccessUrl()時(shí),才會(huì)被調(diào)用。作用:清除原路徑,使用defaultSuccessUrl()指定的路徑。如果直接使用該處理器,則總跳轉(zhuǎn)到根路徑。
  • ForwardAuthenticationSuccessHandler:請(qǐng)求重定向。只有指定successForwardUrl時(shí)被用到。

要想自定義成功處理器,可以通過(guò)實(shí)現(xiàn)AuthenticationSuccessHandler接口或繼承其子類SavedRequestAwareAuthenticationSuccessHandler來(lái)實(shí)現(xiàn):

  • 實(shí)現(xiàn)AuthenticationSuccessHandler接口

    如果直接返回Json數(shù)據(jù)時(shí),可以實(shí)現(xiàn)AuthenticationSuccessHandler接口:

    public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            Authentication authentication) 
            throws ServletException, IOException {
          response.setContentType("application/json;charset=UTF-8");
          response.getWriter().append(
                  new ObjectMapper().createObjectNode()
                          .put("status", 200)
                          .put("msg", "登錄成功")
                          .toString());
        }
    }
    
  • 繼承SavedRequestAwareAuthenticationSuccessHandler

    如果只是在登錄認(rèn)證后,需要處理數(shù)據(jù),再跳轉(zhuǎn)回原路徑時(shí),可以繼承該類:

    public class CustomAuthenticationSuccessHandler2 extends SavedRequestAwareAuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            Authentication authentication) 
            throws ServletException, IOException {
            // 登錄成功后,進(jìn)行數(shù)據(jù)處理
            System.out.println("用戶登錄成功啦?。。?);
            String authenticationStr = objectMapper.writeValueAsString(authentication);
            System.out.println("用戶登錄信息打?。? + authenticationStr);
    
            //處理完成后,跳轉(zhuǎn)回原請(qǐng)求URL
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
    

Spring Security默認(rèn)是使用SavedRequestAwareAuthenticationSuccessHandler,在配置中修改為自定義的AuthenticationSuccessHandler

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                // 配置使用自定義成功處理器
                .successHandler(new AuthenticationSuccessHandler());
    }
}

1.2.2、自定義AuthenticationFailureHandler

AuthenticationFailureHandler是身份認(rèn)證失敗處理器的接口,其下有多個(gè)子類實(shí)現(xiàn):

  • SimpleUrlAuthenticationFailureHandler:默認(rèn)的失敗處理器,默認(rèn)認(rèn)證失敗后,跳轉(zhuǎn)到登錄頁(yè)路徑加error參數(shù),如:http://localhost:8080/login?error??赏ㄟ^(guò)failureUrl()配置。
  • ForwardAuthenticationFailureHandler:重定向到指定的URL
  • DelegatingAuthenticationFailureHandler:將AuthenticationException子類委托給不同的AuthenticationFailureHandler,意味著可以為AuthenticationException的不同實(shí)例創(chuàng)建不同的行為
  • ExceptionMappingAuthenticationFailureHandler:可以根據(jù)不同的AuthenticationException 類型,設(shè)置不同的跳轉(zhuǎn) url

自定義失敗處理器,可以通過(guò)實(shí)現(xiàn)AuthenticationFailureHandler接口或繼承其子類SimpleUrlAuthenticationFailureHandler來(lái)實(shí)現(xiàn):

  • 實(shí)現(xiàn)AuthenticationFailureHandler接口:

    public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            AuthenticationException exception) 
            throws IOException, ServletException {
          response.setContentType("application/json;charset=UTF-8");
          response.getWriter().append(
                  new ObjectMapper().createObjectNode()
                          .put("status", 401)
                          .put("msg", "用戶名或密碼錯(cuò)誤")
                          .toString());
        }
    }
    
  • 繼承SimpleUrlAuthenticationFailureHandler

    public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {  
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            AuthenticationException exception) 
            throws IOException, ServletException {
            // 登錄失敗后,進(jìn)行數(shù)據(jù)處理
            System.out.println("登錄失敗啦!?。?);
            String exceptionStr = objectMapper.writeValueAsString(exception.getMessage());
            System.out.println(exceptionStr);
    
            // 跳轉(zhuǎn)原頁(yè)面
            super.onAuthenticationFailure(request, response, exception);
        }
    }
    

Spring Security默認(rèn)驗(yàn)證失敗是使用SimpleUrlAuthenticationFailureHandler,在配置中修改為自定義的AuthenticationFailureHandler

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                // 配置使用自定義失敗處理器
                .failureHandler(new AuthenticationFailureHandler());
    }
}

這里順便提及DelegatingAuthenticationFailureHandlerExceptionMappingAuthenticationFailureHandler的使用:

  • DelegatingAuthenticationFailureHandler

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...
        @Bean
        public DelegatingAuthenticationFailureHandler delegatingAuthenticationFailureHandler(){
            LinkedHashMap<Class<? extends AuthenticationException>, AuthenticationFailureHandler> handlers = new LinkedHashMap<>();
            // 登錄失敗時(shí),使用的失敗處理器
            handlers.put(BadCredentialsException.class, new BadCredentialsAuthenticationFailureHandler());
            // 用戶過(guò)期時(shí),使用的失敗處理器
            handlers.put(AccountExpiredException.class, new AccountExpiredAuthenticationFailureHandler());
            // 用戶被鎖定時(shí),使用的失敗處理
            handlers.put(LockedException.class, new LockedAuthenticationFailureHandler());
            return new DelegatingAuthenticationFailureHandler(handlers, new AuthenticationFailureHandler());
        }
        
      @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                  // 配置使用自定義失敗處理器
                    .failureHandler(delegatingAuthenticationFailureHandler());
        }
    }
    
  • ExceptionMappingAuthenticationFailureHandler

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...
        @Bean
        public ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler(){
            ExceptionMappingAuthenticationFailureHandler handler = new ExceptionMappingAuthenticationFailureHandler();
            HashMap<String, String> map = new HashMap<>();
            // 登錄失敗時(shí),跳轉(zhuǎn)到 /badCredentials
            map.put(BadCredentialsException.class.getName(), "/badCredentials");
            // 用戶過(guò)期時(shí),跳轉(zhuǎn)到 /accountExpired
            map.put(AccountExpiredException.class.getName(), "/accountExpired");
            // 用戶被鎖定時(shí),跳轉(zhuǎn)到 /locked
            map.put(LockedException.class.getName(), "/locked");
            handler.setExceptionMappings(map);
            return handler;
        }
        
      @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                  // 配置使用自定義失敗處理器
                    .failureHandler(exceptionMappingAuthenticationFailureHandler());
        }
    }
    

1.3、認(rèn)證入口

AuthenticationEntryPointSpring Security認(rèn)證入口點(diǎn)接口,在用戶請(qǐng)求處理過(guò)程中遇到認(rèn)證異常時(shí),使用特定認(rèn)證方式進(jìn)行認(rèn)證。

AuthenticationEntryPoint內(nèi)置實(shí)現(xiàn)類:

  • LoginUrlAuthenticationEntryPoint:根據(jù)配置的登錄頁(yè)面url,將用戶重定向到該登錄頁(yè)面進(jìn)行認(rèn)證。默認(rèn)的認(rèn)證方式。

  • Http403ForbiddenEntryPoint:設(shè)置響應(yīng)狀態(tài)為403,不觸發(fā)認(rèn)證。通常在預(yù)身份認(rèn)證中設(shè)置

    在某些情況下,使用Spring Security進(jìn)行授權(quán),但是在訪問(wèn)該應(yīng)用程序之前,某些外部系統(tǒng)已經(jīng)對(duì)該用戶進(jìn)行了可靠的身份驗(yàn)證。這些情況稱為“預(yù)身份驗(yàn)證(pre-authenticated)”。

  • HttpStatusEntryPoint:設(shè)置特定的響應(yīng)狀態(tài)碼,不觸發(fā)認(rèn)證。

  • BasicAuthenticationEntryPoint:設(shè)置基本(Http Basic)認(rèn)證,在響應(yīng)狀態(tài)碼401HeaderWWW-Authenticate:"Basic realm="xxx"時(shí)使用。

  • DigestAuthenticationEntryPoint:設(shè)置摘要(Http Digest)認(rèn)證,在響應(yīng)狀態(tài)碼401HeaderWWW-Authenticate:"Digest realm="xxx"時(shí)使用。

  • DelegatingAuthenticationEntryPoint:根據(jù)匹配URI來(lái)委托給不同的AuthenticationEntryPoint,且必須制定一個(gè)默認(rèn)的認(rèn)證方式。

1.3.1、自定義AuthenticationEntryPoint

  1. 自定義處理,需要新建類實(shí)現(xiàn)該AuthenticationEntryPoint接口:

    public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
         response.setContentType("application/json;charset=UTF-8");
         response.getWriter().append(
                 new ObjectMapper().createObjectNode()
                         .put("status", 401)
                         .put("msg", "未登錄,請(qǐng)登錄后訪問(wèn)")
                         .toString());
        }
    }
    
  2. WebSecurityConfig配置:

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...       
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 指定未登錄入口點(diǎn)
            http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
            ...
        }
    }
    

其它子類的用法:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Bean
    public DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
        LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<>();
        // GET方式請(qǐng)求/test時(shí),直接返回 403
        map.put(new AntPathRequestMatcher("/test", "GET"), new Http403ForbiddenEntryPoint());
        // 訪問(wèn) /basic時(shí),直接返回 400 bad request
        map.put(new AntPathRequestMatcher("/basic"), 
                new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
        DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(map);
        // 除了上面兩個(gè) uri 配置指定的認(rèn)證入口,其它默認(rèn)使用 LoginUrlAuthenticationEntryPoint認(rèn)證入口
        entryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint("/user-login"));
        return entryPoint;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * Http403ForbiddenEntryPoint 用法
         */
        // http.exceptionHandling()
        //     .authenticationEntryPoint(new Http403ForbiddenEntryPoint());
        /** 
         * HttpStatusEntryPoint 用法
         */
        // http.exceptionHandling()
        //     .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
        /**
         * DelegatingAuthenticationEntryPoint 用法
         */
        http.exceptionHandling()
            .authenticationEntryPoint(delegatingAuthenticationEntryPoint());
        ...
    }
}

而對(duì)于摘要認(rèn)證DigestAuthenticationEntryPoint,因?yàn)?code>Http摘要認(rèn)證必須基于MD5或明文,不能使用其它加密方式,且加密方式是MD5(username:realm:password),所以我們需要手動(dòng)加密用戶密碼:

public int addUser(UserInfo userInfo) throws NoSuchAlgorithmException {
        String username = userInfo.getUsername();
        String password = userInfo.getPassword();

        // 加密密碼
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        String realm = "realm";  // 默認(rèn)是 readlm
        String userData = username + ":" + realm + ":" + password;
        password = new String(Hex.encode(md5.digest(userData.getBytes())));

        userInfo.setPassword(password);
        return userMapper.addUser(userInfo);
}

WebSecurityConfig配置中:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 因?yàn)橐呀?jīng)使用摘要認(rèn)證MD5加密,不用再加密,所以這里設(shè)置為明文
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        };
    }
    
    // 摘要認(rèn)證的過(guò)濾器
    @Bean
    public DigestAuthenticationFilter digestAuthenticationFilter() {
        DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
        filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());//必須配置
        filter.setPasswordAlreadyEncoded(true); // 密碼需要加密,設(shè)為true
        filter.setUserDetailsService(userDetailsService);//必須配置
        return filter;
    }

    @Bean
    public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
        DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
        point.setRealmName("realm");//realm名稱,默認(rèn)為realm,該名稱和加密密碼的realm一樣
        return point;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 使用摘要認(rèn)證的入口
                .exceptionHandling().authenticationEntryPoint(digestAuthenticationEntryPoint())
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/addUser").permitAll()
                .antMatchers("/hello2").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                // 摘要認(rèn)證的過(guò)濾器
                .addFilter(digestAuthenticationFilter())
}

1.4、無(wú)權(quán)限處理器

自定義處理,需要新建類實(shí)現(xiàn)該AccessDeniedHandler接口:

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().append(
                new ObjectMapper().createObjectNode()
                        .put("status", 401)
                        .put("msg", "無(wú)訪問(wèn)權(quán)限")
                        .toString());
    }
}

WebSecurityConfig配置:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    ...    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 先注釋,用登錄頁(yè)面登錄
        //http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
        http.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

啟動(dòng)程序,訪問(wèn)localhost:8080/get-user,跳轉(zhuǎn)登錄頁(yè)面,輸入用戶名、密碼登錄后,訪問(wèn)無(wú)權(quán)限的資源,會(huì)返回?zé)o權(quán)限Json信息:

圖片

1.5、記住登錄

Spring Security記住登錄功能有兩種方式:基于瀏覽器的Cookie存儲(chǔ)和基于數(shù)據(jù)庫(kù)的存儲(chǔ)。

登錄頁(yè)添加記住登錄按鈕

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>登錄頁(yè)面</title>
</head>
<body>
<div th:if="${param.error}">
    <p>用戶名或密碼無(wú)效</p>
</div>
<form th:action="@{/my-login}" method="post">
    <div><label> 用戶名 : <input type="text" name="username"/> </label></div>
    <div><label> 密碼: <input type="password" name="password"/> </label></div>
    <div>
        <label><input type="checkbox" name="remember-me"/>記住登錄</label>
        <button type="submit">登錄</button>
    </div>
</form>
</body>
</html>

1.5.1、Cookie存儲(chǔ)

WebSecurityConfig配置:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/user-login").permitAll()
                .loginProcessingUrl("/my-login")
                .successHandler(new CustomAuthenticationSuccessHandler())
                .failureHandler(new CustomAuthenticationFailureHandler())
                .and()
                .rememberMe()
                // 即登錄頁(yè)面的記住登錄按鈕的參數(shù)名
                .rememberMeParameter("remember-me")
                // 過(guò)期時(shí)間
                .tokenValiditySeconds(1800)
                .and()
                .csrf().disable();
    }
}

啟動(dòng)程序,在勾選記住登錄下進(jìn)行登錄,cookie信息如下,remember-me的過(guò)期時(shí)間內(nèi),重啟瀏覽器訪問(wèn)不用登錄。


圖片

1.5.2、數(shù)據(jù)庫(kù)存儲(chǔ)

使用 Cookie 存儲(chǔ)雖然很方便,但是Cookie畢竟是保存在客戶端的,而且 Cookie 的值還與用戶名、密碼這些敏感數(shù)據(jù)相關(guān),雖然加密,但是將敏感信息存在客戶端,畢竟不太安全。

Spring security 還提供了另一種更安全的實(shí)現(xiàn)機(jī)制:在客戶端的 Cookie 中,僅保存一個(gè)無(wú)意義的加密串(與用戶名、密碼等敏感數(shù)據(jù)無(wú)關(guān)),然后在數(shù)據(jù)庫(kù)中保存該加密串-用戶信息的對(duì)應(yīng)關(guān)系,自動(dòng)登錄時(shí),用 Cookie 中的加密串,到數(shù)據(jù)庫(kù)中驗(yàn)證,如果通過(guò),自動(dòng)登錄才算通過(guò)。

WebSecurityConfig 中注入 dataSource ,創(chuàng)建一個(gè) PersistentTokenRepositoryBean,并配置數(shù)據(jù)庫(kù)存儲(chǔ)自動(dòng)登錄:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 啟動(dòng)時(shí)創(chuàng)建表,注意,創(chuàng)建好表后,注釋掉
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login").permitAll()
                .and()
                // 記住登錄
                .rememberMe()
                // 記住我的數(shù)據(jù)存儲(chǔ),調(diào)用上面寫的方法
                .tokenRepository(persistentTokenRepository())
                // 過(guò)期時(shí)間
                .tokenValiditySeconds(1800)
                .and()
                .csrf().disable();
    }
}

二、session管理

在執(zhí)行認(rèn)證過(guò)程之前,Spring Security將運(yùn)行SecurityContextPersistenceFilter過(guò)濾器負(fù)責(zé)存儲(chǔ)安請(qǐng)求之間的全上下文,上下文根據(jù)策略進(jìn)行存儲(chǔ),默認(rèn)為HttpSessionSecurityContextRepository ,其使用http session作為存儲(chǔ)器。

對(duì)于session管理,有三種:

  1. session超時(shí)處理:session有效的時(shí)間,超時(shí)后刪除
  2. session并發(fā)控制:同個(gè)用戶登錄,是否強(qiáng)制退出前一個(gè)登錄,還是禁止后一個(gè)登錄。
  3. 集群session管理:默認(rèn)session是放在單個(gè)服務(wù)器的單個(gè)應(yīng)用里,在集群中,會(huì)出現(xiàn)在一個(gè)節(jié)點(diǎn)應(yīng)用登錄后,session只能在該節(jié)點(diǎn)使用。另一個(gè)節(jié)點(diǎn)不能使用其他節(jié)點(diǎn)的session,還會(huì)需要登錄,所以需要集群共用一個(gè)session

2.1、session超時(shí)

設(shè)置Session的超時(shí),很簡(jiǎn)單,只需要在配置文件application.yml配置即可,如下為設(shè)置50秒:

  • Springboot2.0前的版本:
spring:
  session:
    timeout: 50
  • Springboot2.0后的版本:
server:
  servlet:
    session:
      timeout: 50

上面設(shè)置Session失效時(shí)間為50s,實(shí)際源碼TomcatEmbeddedServletContainerFactory類內(nèi)部會(huì)取1分鐘。源碼內(nèi)部轉(zhuǎn)成分鐘,然后設(shè)置給tomcat原生的StandardContext,所以一般設(shè)置為60秒的整數(shù)倍。

其實(shí)通過(guò)上面配置的點(diǎn)擊進(jìn)去源碼發(fā)現(xiàn):

public void setTimeout(Duration timeout) {
    this.timeout = timeout;
}

參數(shù)傳入的是Duration的實(shí)例,DurationJava8新增的,用來(lái)計(jì)算日期差值,并且是被final聲明,是線程安全的

Duration轉(zhuǎn)換字符串方式,默認(rèn)為正;負(fù)以-開(kāi)頭,緊接著P。

以下字母不區(qū)分大小寫:

  • D:天
  • T:天和小時(shí)之間的分隔符
  • H:小時(shí)
  • M:分鐘
  • S:秒

每個(gè)單位都必須是數(shù)字,且時(shí)分秒順序不能亂
比如:

  • P2DT3M5S235
  • P3D:3`天
  • PT3H3小時(shí)

所以上面配置文件中可以寫:

server:
  servlet:
    session:
      timeout: PT50S

2.2、session超時(shí)處理

2.2.1、超時(shí)跳轉(zhuǎn)URL

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // session無(wú)效時(shí)跳轉(zhuǎn)的url
            http.sessionManagement().invalidSessionUrl("/session/invalid");
            http
                .authorizeRequests()
                // 需要放行條跳轉(zhuǎn)的url
                .antMatchers("/session/invalid").permitAll()
                .anyRequest().authenticated()
        }
    }
}

2.2.2、超時(shí)處理器

session無(wú)效時(shí)的處理策略,優(yōu)先級(jí)比上面的高

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomInvalidSessionStrategy invalidSessionStrategy;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 設(shè)置session無(wú)效處理策略
        http.sessionManagement().invalidSessionStrategy(invalidSessionStrategy);
        http
                .authorizeRequests()
                .antMatchers("/session/invalid").permitAll()
                .anyRequest().authenticated()
    }
}

處理策略:

@Component
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // 自定義session無(wú)效處理
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().append("session無(wú)效,請(qǐng)重新登錄");
    }
}

2.3、session并發(fā)控制

默認(rèn)下,我們可以在不同瀏覽器同時(shí)登錄同一個(gè)用戶,這樣就會(huì)保存了多個(gè)Session,而有時(shí),我們需要只能在一處地方登錄,其他地方的登錄就讓前一個(gè)失效或不能登錄。

2.3.1、后登錄致前登錄失效

在一個(gè)瀏覽器登錄后,再到另一個(gè)瀏覽器登錄,再回到前一個(gè)登錄刷新頁(yè)面,登錄失效。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
             // 設(shè)置session無(wú)效處理策略
            .invalidSessionStrategy(invalidSessionStrategy)
            // 設(shè)置同一個(gè)用戶只能有一個(gè)登陸session
            .maximumSessions(1);
        http
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}

上面設(shè)置maximumSessions設(shè)置為1后,只能有一個(gè)登錄Session,多個(gè)登錄,后一個(gè)會(huì)把前一個(gè)登錄的Sesson失效。

而對(duì)于前一個(gè)登錄Sesson失效后,刷新頁(yè)面會(huì)顯示:

This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

我們也可以自定義失效返回信息,有兩種

  1. 設(shè)置失效session處理URL:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.sessionManagement()
                    .invalidSessionStrategy(invalidSessionStrategy)
                    .maximumSessions(1)
                    // 其他地方登錄session失效處理URL
                    .expiredUrl("/session/expired");
            http
                    .authorizeRequests()
                 // URL不需驗(yàn)證
                    .antMatchers("/session/expired").permitAll()
                    .anyRequest().authenticated()
        }
    }
    
  2. 設(shè)置失效session處理策略:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
        @Autowired
        private CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.sessionManagement()
                    .invalidSessionStrategy(invalidSessionStrategy)
                    .maximumSessions(1)
                    // 其他地方登錄session失效處理策略
                    .expiredSessionStrategy(sessionInformationExpiredStrategy);
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
        }
    }
    

    過(guò)期策略:

    @Component
    public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
            HttpServletResponse response = event.getResponse();
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("當(dāng)前用戶已在其他地方登錄...");
        }
    }
    

2.3.2、前登錄禁后登錄

有時(shí),我們?cè)谝粋€(gè)地方登錄正在操作,不能被打斷,這時(shí)就要禁止在其他地方登錄導(dǎo)致當(dāng)前的登錄Session失效。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
            .invalidSessionStrategy(invalidSessionStrategy)
            .maximumSessions(1)
            // 設(shè)置為true,即禁止后面其它人的登錄 
            .maxSessionsPreventsLogin(true)
            .expiredSessionStrategy(sessionInformationExpiredStrategy);
        http
            .authorizeRequests()
            .anyRequest().authenticated()
    }
}

禁止后登錄后,可以通過(guò)如下方式判斷異常進(jìn)行用戶通知:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        AuthenticationException exception) {
        response.setContentType("application/json;charset=utf-8");
        if (exception instanceof SessionAuthenticationException){
            response.getWriter().write("用戶已在其它地方登錄,禁止當(dāng)前登錄...");
        }
    }
}

2.4、集群session管理

在部署應(yīng)用時(shí),搭建至少兩臺(tái)機(jī)器的集群環(huán)境,防止一臺(tái)服務(wù)器出現(xiàn)問(wèn)題而服務(wù)中斷,這樣在一臺(tái)機(jī)器在停止服務(wù)時(shí),另一臺(tái)機(jī)器還能繼續(xù)提供服務(wù)。

而使用集群,在基于Session的身份認(rèn)證就會(huì)導(dǎo)致問(wèn)題:一個(gè)用戶登錄成功后,其Session存放在A機(jī)器上,而如果Session不做其他處理,在用戶操作時(shí),在負(fù)載均衡下,可能會(huì)請(qǐng)求發(fā)到B機(jī)器上,而B機(jī)器無(wú)Session導(dǎo)致無(wú)權(quán)限訪問(wèn)而需要再次登錄。

而解決集群中Session的管理,可以把Session抽取出來(lái)為一個(gè)獨(dú)立存儲(chǔ),用戶請(qǐng)求需要Session時(shí)都會(huì)讀取該存儲(chǔ)Session

1585560853325

Spring提供有Spring Session來(lái)處理集群Session管理,需要引入如下依賴:

<dependency>
     <groupId>org.springframework.session</groupId>
     <artifactId>spring-session-data-redis</artifactId>
</dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

使用redis作為Session存儲(chǔ)管理,而Spring Session支持以下方式存儲(chǔ)Session,這里只使用Redis。

public enum StoreType {
    REDIS,
    MONGODB,
    JDBC,
    HAZELCAST,
    NONE;
    private StoreType() {
    }
}

在配置文件application.yml中配置Redis:

spring:
  session:
    store-type: redis   # session存儲(chǔ)類型為 redis
  redis:
    database: 1
    host: localhost
    port: 6379
    # 更新策略,ON_SAVE在調(diào)用#SessionRepository#save(Session)時(shí),在response commit前刷新緩存,
    # IMMEDIATE只要有任何更新就會(huì)刷新緩存
    flush-mode: on_save  # 默認(rèn)
    # 存儲(chǔ)session的密鑰的命名空間
    namespace: spring:session #默認(rèn)

以不同的端口啟動(dòng)程序,如分別以端口80808081啟動(dòng)兩個(gè)服務(wù)。訪問(wèn)8080端口登錄后,在訪問(wèn)8081就不需要登錄了,說(shuō)明Session被共用了。

二、退出登錄

默認(rèn)的退出登錄URL/logout,如前面登錄的程序,訪問(wèn)localhost:8080/logout便退出登錄,退出登錄后,默認(rèn)跳轉(zhuǎn)到登錄頁(yè)面。

2.1、自定義退出URL

也可通過(guò)在WebSecurityConfig進(jìn)行自定義配置:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout()
                // 退出登錄的url, 默認(rèn)為/logout
                .logoutUrl("/logout2")
    }
}

2.2、退出成功處理

  1. 退出成功處理URL:

    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .logout()
                    // 退出登錄的url, 默認(rèn)為/logout
                    .logoutUrl("/logout2")
                 // 退出成功跳轉(zhuǎn)URL,注意該URL不需要權(quán)限驗(yàn)證
                    .logoutSuccessUrl("/logout/success").permitAll()
        }
    }
    
  2. 退出成功處理器

    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .logout()
                    // 退出登錄的url, 默認(rèn)為/logout
                    .logoutUrl("/logout2")
                 // 退出成功跳轉(zhuǎn)URL,注意該URL不需要權(quán)限驗(yàn)證,所有加.permitAll
                    //.logoutSuccessUrl("/logout/success").permitAll()
                 //退出登錄成功處理器
                    .logoutSuccessHandler(logoutSuccessHandler)
        }
    }
    

    處理器:

    @Component
    public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("退出登錄成功");
        }
    }
    

2.3、退出成功刪除Cookie

默認(rèn)退出后不會(huì)刪除Cookie??膳渲猛顺龊髣h除:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout()
                // 退出登錄的url, 默認(rèn)為/logout
                .logoutUrl("/logout2")
                // 退出成功跳轉(zhuǎn)URL,注意該URL不需要權(quán)限驗(yàn)證,所有加.permitAll
                //.logoutSuccessUrl("/logout/success").permitAll()
                //退出登錄成功處理器
                .logoutSuccessHandler(logoutSuccessHandler)
                // 退出登錄刪除指定的cookie
                .deleteCookies("JSESSIONID")
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容