【手摸手,帶你搭建前後端分離商城系統】03 整合Spring Security token 實現方案,完成主業務登入
全域性異常攔截器,攔截自定義ApiException *
author: MRC * * @param e 自定義異常 * @return xyz.chaobei.common.api.CommonResult * @since 2020/10/20 **/ @ExceptionHandler(value = ApiException.class) public CommonResult exceptionHandler(ApiException e) { log.info("系統異常攔截器:異常資訊:" + e.getMessage()); if (Objects.nonNull(e.getErrorCode())) { return CommonResult.failed(e.getErrorCode()); } return CommonResult.failed(e.getMessage()); } } ``` 直接通過 `return` 的方式,就好像我們在 `controller` 裡面給前端返回`json` 一樣簡單。 斷言則是,判斷某一條件是否成立、如果不成立則丟擲異常的一種更加簡單的方式。就不用每次都寫`throw new xxxException` 簡而言之就是:一種非常優美的方式拋異常(偷懶的) ```java public class Asserts { /** *
斷言丟擲一個異常 *
author: MRC
*
* @param message 提示語
* @return void
* @since 2020/10/15
**/
public static void fail(String message) {
throw new ApiException(message);
}
public static void fail(IErrorCode iErrorCode) {
throw new ApiException(iErrorCode);
}
}
```
#### Spring Security UserDetails
Spring UserDetails 作為一個介面、規定了一些需要的引數方法。我們必須要用自己的邏輯實現這個方法。並將`username` `password` 等重要資訊通過其定義的方法進行返回。也是作為一種橋接、將我們的使用者名稱、密碼等資訊交付給 `SpringSecurity`
```java
public class UmsAdminUserDetails implements UserDetails {
private final UmsAdminModel adminModel;
public UmsAdminUserDetails(UmsAdminModel adminModel) {
this.adminModel = adminModel;
}
// 省略,具體請檢視原始碼
}
```
### JWT 簽發服務
`JWT` 又稱作`JsonWebToken` ,我們需要一個依賴來生成token/登入後需要將這個 `token` 返回給前端,讓前端儲存,而後所有的請求都需要帶上這個 `token` 然後我們服務端就知道是哪個使用者在請求了。
```xml
從toKen中獲取負載資訊
* author: MRC
*
* @param token 獲取的token
* @return io.jsonwebtoken.Claims
* @since 2020/10/22
**/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.info("JWT格式驗證失敗:{}", token);
}
return claims;
}
```
該方法描述瞭如何從一個`token` 裡面取出我們所需要的 `Claims` 資訊。並且可以從負載裡面取出 `sub` 以及 `exp` 等資訊。我簡要介紹一個。其他的詳細內容請檢視原始碼。
```java
/**
* 首先獲取token當中的負載、而後從負載中取出sub
* author: MRC
*
* @param token 被校驗的token
* @return java.lang.String
* @since 2020/10/22
**/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
```
**如果你的token被篡改了,那麼驗證的時候肯定會報錯、所以要捕獲一下異常。返回空即可。**
### login service
寫到這裡,我們`login` 控制器的`service` 已經可以全部寫下去了。登入成功,通過`tokenService` 返回一個token ,然後封裝返回給前端即可。
```java
@Override
public UmsAdminTokenBO umsAdminLogin(UmsAdminLoginParam param) {
// 通過使用者名稱獲取userDetail
UserDetails userDetails = this.findUserDetailByUserName(param.getUsername());
// 基本校驗使用者名稱和密碼
if (!passwordEncoder.matches(param.getPassword(), userDetails.getPassword())) {
Asserts.fail("使用者名稱密碼錯誤");
}
// 這裡暫時不開啟許可權,後面再修改
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null);
// 將構建的使用者資訊加入spring security context 上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = defaultTokenServer.generateToken(userDetails);
return UmsAdminTokenBO.builder().token(token).tokenHeader(jwtConfig.getTokenHeader()).build();
}
```
### Security Config
接下來。就是配置一個全域性的`Security Config`
```java
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
```
主要還是需要重寫`configure()` 方法。獲取一個 `registry` 例項。將我們的攔截資訊加入到裡面。
- 配置開放的路徑
- 配置需要驗證的路徑。
- 新增一個JWT預設過濾器,在`SpringSecurity` 處理之前,將token 進行校驗後加入到`context` 上下文裡面。
```java
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer token 過濾器邏輯
* 1、token 必須存在
* 2、toKen 必須正確,未過期。
* 3、若上下文不存在。則往上下文放一個userDetail
* author: MRC
*
* @param request 請求
* @param response 響應
* @param filterChain 過濾器
* @return void
* @since 2020/10/22
**/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(jwtConfig.getTokenHeader());
log.info("doFilterInternal request url={}", request.getRequestURL());
log.info("doFilterInternal request token={}", token);
// 請求攜帶token/則檢驗這個token是否正確和是否過期
if (!StringUtils.isEmpty(token)) {
// 攜帶的使用者名稱資訊
String username = defaultTokenServer.getUserNameFromToken(token);
log.info("request token username={}", username);
if (StringUtils.isEmpty(username)) {
filterChain.doFilter(request, response);
}
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//校驗token是否有效
if (defaultTokenServer.isTokenExpired(token)) {
filterChain.doFilter(request, response);
}
//檢查當前上下文是否存在使用者資訊,若沒有則新增
if (SecurityContextHolder.getContext().getAuthentication() == null) {
log.info("doFilterInternal getContext = null");
// 將使用者資訊新增到上下文。說明這個request 是通過的。
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("doFilterInternal user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// 通過攔截器
filterChain.doFilter(request, response);
}
}
```
其實我們這裡去掉`session` 以後,我們的客戶端對於前端的請求標識、只能通過攜帶token的方式。
然後我們每一個請求首先會進入`JwtAuthenticationTokenFilter` 也就是我們上面寫的這個。
檢查當前請求有沒有攜帶`token` 要是帶了 `token` 那就檢查它,檢查成功就從資料庫查出來這個人。把這個人注入到我們的`SpringSecurity Context` 裡面。
`SpringSecurity` 的其他過濾器看到上下文有東西在,就放行~說明是登入後的。
要是沒帶、或者驗證錯誤~。那上下文也就沒有這個使用者的資訊了。所以這個請求只能返回`403`
### 密碼問題
這裡使用的是:`PasswordEncoder` 介面實現類下的 `BCryptPasswordEncoder` ,當然,你肯定要在使用之前要用`@Bean`
```java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
```
未來使用的時候、直接注入一個就行了。
- `matches` 校驗
- `encode` 加密
至於是怎麼加密的。當然還得研究一下~
### 實際測試
在未登入之前,我們訪問一個介面~
```json
{
"code": 401,
"data": "Full authentication is required to access this resource",
"message": "暫未登入或token已經過期"
}
```
首先使用使用者名稱和密碼進行登入,我們加入一條資料。`admin,123456`
```sql
INSERT INTO `mall-pro`.`ums_admin`(`id`, `username`, `password`, `icon`, `lock`, `email`, `nick_name`, `note`, `create_time`, `login_time`, `status`) VALUES (1, 'admin', '$2a$10$08arRlZRspTqMBK1N8NqW.9CQq7KWffa47MGelgJMuPK/uXtKX3O6', '#e', 1, '[email protected]', '管理員', '測試', '2020-10-22 16:14:33', '2020-10-22 16:14:36', 1);
```
請求登入介面`/auth/login` ,驗證使用者名稱和密碼後、返回資訊如下:
```json
{
"code": 200,
"message": "操作成功",
"data": {
"tokenHeader": "Authorization",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6Im1hbGwtcHJvIiwiZXhwIjoxNjAzNTAzNjU3LCJpYXQiOjE2MDM0MTcyNTc4MzJ9.5bX2gajbRebS9MyII3OlBKD4xc5uTgelvFprT8SHvBq_MnFa--CSn3ntkGteITt5lLRbAyxyzC8u8KZ1ZCdYjg"
}
}
```
將登入後,將指定頭和token帶入請求頭進行請求,成功請求到資料~
### 小結
已經好久沒更新這一篇文章了。希望我的讀者你們不要怪我,實在是太忙了。白天要上班,偶爾摸魚寫一寫,程式碼除錯完、而後我再整理這篇文章。現在已經是凌晨00:26 。加油吧~ 我努力更新完這個系列。
#### 原始碼地址
https://gitee.com/mrc1999/mall-pro
#### 歡迎關注
![](https://file.chaobei.xyz/blogs/banner_1591192617