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.運行項目
賬號密碼錯誤情況下: