Springboot整合SpringSecurity二

SpringBoot整合SpringSecurity二

在上一章入門案例 中,我們實現了入門程序,本篇我們在上一章的基礎上完成自動登錄功能及異常處理。

本案例源碼地址:https://gitee.com/lin8081/LWH11

1.自動登錄

所謂自動登陸就是當用戶第一次訪問網站時,輸入用戶名和密碼,然後勾選了自動登陸複選框,進入首頁後,點擊退出登陸,關閉網頁,再次打開同樣的網站,則無需再次輸入賬號密碼,直接進入首頁,這種交互方式就是“自動登錄”。

1.修改登錄頁面:login.html

<form method="post" action="/login">

<!-- 賬號 -->
<div class="layui-form-item">
     <label class="layui-form-label">賬號</label>
     <div class="layui-input-block">
        <input name="username" id="userName"  value="admin" placeholder="默認賬號:admin" type="text" lay-verify="required" class="layui-input">
     </div>
</div>

<!-- 密碼 -->
<div class="layui-form-item">
     <label class="layui-form-label">密碼</label>
      <div class="layui-input-block">
          <input name="password" id="password"  value="123" placeholder="默認密碼:123" type="password" lay-verify="required" class="layui-input">
      </div>
</div>
  
<!-- 自動登錄 -->  
<div class="layui-form-item">
      <label><input style="display: inline" type="checkbox" name="remember-me"/>自動登錄</label>
</div>
                    
<div>
      <button type="submit" class="layui-btn layui-btn-fluid" >登 錄</button>
</div>

</form>

2.自動登錄的實現方式—數據庫存儲

使用 Cookie 存儲雖然方便,但是 Cookie 畢竟是保存在客戶端的,而且 Cookie 的值還與用戶名、密碼這些敏感信息有關,雖然加密了,但是將這些敏感信息存在客戶端,畢竟不太保險。

**SpringSecurity 還提供了另一種相對安全的實現機制: **

  • 在客戶端的 Cookie中,僅保存一個無意義的加密串(與用戶名和密碼等敏感信息無關),然後在數據庫中保存該加密串 - 用戶信息的對應關係,自動登錄時,用 Cookie 中的加密串,到數據庫驗證,如果通過,自動登錄纔算成功。

1.基本原理

當瀏覽器發起表單登錄請求時,當通過 UsernamePasswordAuthenticationFilter 認證成功後,會經過 RememberMeService, 在其中有個 TokenRepository , 它會生成一個 token, 首先將 token 寫入到瀏覽器的 Cookie中,然後將 token、認證成功的用戶名寫入到數據庫中。

當瀏覽器下次請求時,會經過 RememberMeAuthenticationFilter,它會讀取 Cookie 中的 token,交給 RememberMeService ,獲取用戶信息,並將用戶信息放入到 SpringSecurity 中,實現自動登錄。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lk1OUSjB-1584085522201)(assets/1583765716646.png)]

RememberMeAuthenticationFilter在整個過濾器鏈中是比較靠後的位置,也就是說在傳統的登錄方式都無法登錄情況下才會使用自動登錄。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-S4TsyYmA-1584085522204)(assets/1583765815108.png)]

3.創建token數據庫表

可由以下代碼進行創建token表,但數據庫表時已經存在,請註釋掉,否則會報錯

tokenRepository.setCreateTableOnStartup(true);

也可以直接sql語句創建表結構

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

4.代碼實現

在 WebSecurityConfig 中注入 dataSource ,創建一個 PersistentTokenRepository 的Bean對象:

@Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 如果token表不存在,使用下面可以自動初始化表結構,如果已經存在,請註釋掉,否則會報錯
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

在 config() 中配置自動登錄:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允許匿名的url,填在下面
//                .antMatchers().permitAll()
                .anyRequest().authenticated()
                .and()
                // 設置登陸頁
                .formLogin().loginPage("/login")
                // 設置登陸成功頁
                .defaultSuccessUrl("/").permitAll()
                // 自定義登陸用戶名和密碼參數,默認爲username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                .logout().permitAll()

                // 自動登錄
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // token有效時間,單位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 關閉CSRF跨域
        http.csrf().disable();
    }

3.運行測試

勾選自動登錄後,Cookie 和數據庫中均存儲了 token 信息:

在這裏插入圖片描述

2.異常處理

我們登錄失敗的時候,SpringSecurity 幫我們跳轉到了 /login?error URL,奇怪的是不管是控制檯還是網頁上都沒有打印錯誤信息。

這是因爲首先 /login?error 是SpringSecurity 默認的失敗 URL,其次如果你不自己處理這個異常,這個異常時不會被處理的。

1.常見異常

我們先來列舉下一些 SpringSecurity 中常見的異常:

  • UsernameNotFoundException (用戶不存在)
  • DisableException(用戶已被禁用)
  • BadCredentialsException(壞的憑據)
  • LockedException(賬號鎖定)
  • CerdentialsExpiredException(證書過期)
  • … 以上列出的這些異常都是 AuthenticationException 的子類,然後我們看 SpringSecurity 是如何處理 AuthenticationException 異常的。

2.源碼分析

SpringSecurity的異常處理是在過濾器中進行的,我們在 AbastrctAuthenticationProcessingFilter 中找到了對 Authentication 的處理:

  • 在 doFilter() 中,捕獲 AuthenticationException 異常,並交給 unsuccessfulAuthentication() 處理。

  • unsuccessfulAuthentication() 中,轉交給了 SimpleUrlAuthenticationFailureHandler 類的 onAuthencicationFailure() 處理。

  • 在 onAuthenticationFailure() 中,首先判斷有沒有設置 defaultFailureUrl

    a. 如果沒有設置,直接返回 401 錯誤,即 HttpStatus.UNAUTHORIZED 的值。 b. 如果設置了,首先執行 saveException() 方法。然後判斷 forwardToDestination 是否爲服務器調整,默認使用重定向即客戶端跳轉。

  • 在 saveException() 方法中,首先判斷 forwardToDestination,如果使用服務器跳轉則寫入Request,客戶端跳轉則寫入 Session。寫入名爲 WebAttributes.AUTHENTICATION_EXCEPTION 常量對應值SPRING_SECURITY_LAST_EXCEPTION,值爲 AuthenticationException 對象。

  • 至此 SpringSecurity 完成了異常處理,總結下流程:

    –> AbstractAuthenticationProcessingFilter.doFilter() –> AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication() –> SimpleUrlAuthenticationFailureHandler.onAuthenticationFailure() –> SimpleUrlAuthenticationFailureHandler.saveException()

3.處理異常

上面通過源碼看着挺複雜,但真正處理起來SpringSecurity爲我們提供了方便的方式,我們只需要指定錯誤的url,然後在該方法中對異常進行處理即可。

  • 指定錯誤url ,在WebSecurityConfig 中添加 .failureUrl("/login/error")
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允許匿名的url,填在下面
//                .antMatchers().permitAll()
                .anyRequest().authenticated()
                .and()
                // 設置登陸頁
                .formLogin().loginPage("/login")
                // 設置登陸成功url
                .defaultSuccessUrl("/").permitAll()
                // 設置登錄失敗url
                .failureUrl("/login/error")
                // 自定義登陸用戶名和密碼參數,默認爲username和password
//                .usernameParameter("username")
//                .passwordParameter("password")
                .and()
                .logout().permitAll()
                // 自動登錄
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                // 有效時間,單位:s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 關閉CSRF跨域
        http.csrf().disable();
    }

  • 在 Controller 中編寫 loginError方法完成異常處理操作:

    @GetMapping("/login/error")
        @ResponseBody
        public Result loginError(HttpServletRequest request) {
            AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
            log.info("authenticationException={}", authenticationException);
            Result result = new Result();
            result.setCode(201);
    
            if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
                result.setMsg("用戶名或密碼錯誤");
            } else if (authenticationException instanceof DisabledException) {
                result.setMsg("用戶已被禁用");
            } else if (authenticationException instanceof LockedException) {
                result.setMsg("賬戶被鎖定");
            } else if (authenticationException instanceof AccountExpiredException) {
                result.setMsg("賬戶過期");
            } else if (authenticationException instanceof CredentialsExpiredException) {
                result.setMsg("證書過期");
            } else {
                result.setMsg("登錄失敗");
            }
            return result;
        }
    

    修改 CustomUserDetailsService loadUserByUsername() 方法的返回值:

     @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 從數據庫中取出用戶信息
            SysUser user = userService.selectByName(username);
    
            // 判斷用戶是否存在
            if(user == null) {
                throw new UsernameNotFoundException("用戶名不存在");
            }
    
            // 添加權限
            List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
            for (SysUserRole userRole : userRoles) {
                SysRole role = roleService.selectById(userRole.getRoleId());
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
    
            // 返回UserDetails實現類
            return new User(user.getUsername(), user.getPassword(),true,
                    true,
                    true,
                    true,
                    authorities);
        }
    

4.運行項目

賬號密碼錯誤情況下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4vfxRcsV-1584085522208)(assets/1583778922802.png)]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章