1. 程式人生 > 其它 >JWT+SpringSecurity登入和許可權管理

JWT+SpringSecurity登入和許可權管理

一、什麼是JWT

說起JWT,我們應該來談一談基於token的認證和傳統的session認證的區別。說起JWT,我們應該來談一談基於token的認證和傳統的session認證的區別。

(1)、session所存在的問題

Session: 每個使用者經過我們的應用認證之後,我們的應用都要在服務端做一次記錄,以方便使用者下次請求的鑑別,通常而言session都是儲存在記憶體中,而隨著認證使用者的增多,服務端的開銷會明顯增大。

擴充套件性: 使用者認證之後,服務端做認證記錄,如果認證的記錄被儲存在記憶體中的話,這意味著使用者下次請求還必須要請求在這臺伺服器上,這樣才能拿到授權的資源,這樣在分散式的應用上,相應的限制了負載均衡器的能力。這也意味著限制了應用的擴充套件能力。

CSRF: 因為是基於cookie來進行使用者識別的, cookie如果被截獲,使用者就會很容易受到跨站請求偽造的攻擊。

(2)、Token的鑑權機制

基於token的鑑權機制類似於http協議也是無狀態的,也就是說token認證機制的應用不需要去考慮使用者在哪一臺伺服器登入了。

(3)、認識Token

JWT是由三段資訊構成的,以 點(.) 分割,每部分都有不同的含義(每段都是用 Base64 編碼的)
第一部分為 頭部(Header)
第二部分為 載荷(Payload)
第三部分為 簽證(Signature)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNjI1NDY3MDY5LCJ1c2VyTmFtZSI6IumBlW_mCIsImlhdCI6MTYyNTQ2NTI2OX0.e_uuksv0b8gqX9HUVEiieLQlKFKcLdxCxovJ3xA3wB8

第一部分通過Base64解碼出的結果是

{
"typ":"JWT",
"alg":"HS256"
}

由此可以得知jwt的頭部承載兩部分資訊 型別和加密演算法

第二部分是用來放主要的儲存資訊的(主要資訊中除了自定義資訊還有標準中註冊的宣告

iss: jwt簽發者
sub: jwt所面向的使用者
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什麼時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。

當然以上是統一標準而已,並非必須用,建議不強制。

第三部分需要base64加密後的header和base64加密後的payload使用.連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。

二、使用JWT

(1)、匯入依賴

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.10.3</version>
</dependency>

(2)、建立JwtUtils工具類

    @Value("{Jwt.secret}")
    private static String secret;


    /**
     簽發物件:這個使用者的id
     簽發時間:現在
     有效時間:30分鐘
     載荷內容:暫時設計為:這個人的名字,這個人的暱稱
     加密金鑰:這個人的id加上一串字串
     */
    public static String createToken(String userId,String userName) {

        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE,30);
        Date expiresDate = nowTime.getTime();
        //簽發物件
        return JWT.create().withAudience(userId)
                //發行時間
                .withIssuedAt(new Date())
                //有效時間
                .withExpiresAt(expiresDate)
                //載荷,隨便寫幾個都可以,也可以理解為自定義引數
                .withClaim("userName", userName)
                //加密
                .sign(Algorithm.HMAC256(secret+"你隨意寫"));
    }

    /**
     * 檢驗合法性,其中secret引數就應該傳入的是使用者的id
     * @param token
     */
    public static void verifyToken(String token){
        DecodedJWT jwt = null;
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"WDNMD")).build();
            jwt = verifier.verify(token);
        } catch (Exception e) {
            //效驗失敗
            //這裡丟擲的異常是我自定義的一個異常,你也可以寫成別的

        }
    }

    /**
     * 獲取簽發物件
     */
    public static String getAudience(String token) {
        String audience = null;
        try {
            audience = JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException j) {
            //這裡是token解析失敗
            
        }
        return audience;
    }


    /**
     * 通過載荷名字獲取載荷的值
     */
    public static Claim getClaimByName(String token, String name){
        return JWT.decode(token).getClaim(name);
    }

三、JWT結合SpringSecurity實現登入鑑權以及許可權管理

(1)、思路

登陸成功返回Token,並把Token儲存到Redis中確保單點登入。使用過濾器校驗Token和許可權

(2)、SpringSecurity配置

由於使用Token進行登入鑑權,就不需要Session了,因此需禁用Session

@Component
@EnableWebSecurity
/**
 * 開啟@EnableGlobalMethodSecurity(prePostEnabled = true)註解
 * 在繼承 WebSecurityConfigurerAdapter 這個類的類上面貼上這個註解
 * 並且prePostEnabled設定為true,@PreAuthorize這個註解才能生效
 * SpringSecurity預設是關閉註解功能的.
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //注入過濾器
    @Resource
    private JwtVerificationFilter jwtVerificationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //關閉csrf防護 >只有關閉了,才能接受來自表單的請求
        http.csrf().disable()
                .cors()//開啟跨域
                .and()
                //開啟授權請求
                .authorizeRequests()
                //放行介面,因為使用自定義登入頁面所以需要放行
                .antMatchers("/login/**").permitAll()
                //攔截所有請求,所有請求都需要登入認證
                .anyRequest().authenticated()
                .and()
                .addFilterAfter(jwtVerificationFilter, UsernamePasswordAuthenticationFilter.class)
                //前後端分離採用JWT 不需要session(新增後Spring永遠不會建立session)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

(3)、編寫過濾器

/**
 * @author admin
 * 過濾器 發起請求前檢驗Token 實現並在每次請求時只執行一次過濾
 * 在spring中,filter都預設繼承OncePerRequestFilter
 * OncePerRequestFilter顧名思義,他能夠確保在一次請求只通過一次filter,而不需要重複執行
 * 為了相容不同的web container,特意而為之
 *
 * 在servlet2.3中,Filter會經過一切請求,包括伺服器內部使用的forward轉發請求和<%@ include file=”/login.jsp”%>的情況
 *
 * servlet2.4中的Filter預設情況下只過濾外部提交的請求,forward和include這些內部轉發都不會被過濾,
 */
@Component
@Slf4j
public class JwtVerificationFilter extends OncePerRequestFilter {
    @Resource
    private RoleService roleService;
    @Resource
    private PermissionService permissionService;
    @Resource
    private RolePermissionService rolePermissionService;

    /**
     * 過濾器,檢驗Token
     * 發起請求時會呼叫兩次,第二次是展示/favicon.ico
     *
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, @NotNull FilterChain filterChain) throws ServletException, IOException {
        //獲取Token
        String token = httpServletRequest.getHeader("token");

        //非空校驗
        if (token == null) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        //檢驗Token合法性
        JwtUtils.getAudience(token);
        //比對Redis中儲存的Token
        String redisToken = RedisUtils.get(RedisPrefixKey.LOGIN_TOKEN.keyAppend(JwtUtils.getAudience(token)).getKey())
                .toString();
        if (!redisToken.equals(token)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        //獲取許可權                                                             根據Token獲取載荷的值
        List<GrantedAuthority> authorityList = this.findAllAuthority(Long.valueOf(JwtUtils.getAudience(token)));

        //安全上下文,儲存認證授權的相關資訊,實際上就是儲存"當前使用者"賬號資訊和相關許可權
        SecurityContextHolder
                .getContext()
                .setAuthentication(new UsernamePasswordAuthenticationToken(null,null,authorityList));


        //將請求轉發給過濾器鏈下一個filter
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    /**
     * 查詢許可權
     */
    private List<GrantedAuthority> findAllAuthority(Long userId){
        //1、拿到使用者的角色和許可權
        //2、返回的許可權
        List<GrantedAuthority> authorityList = new ArrayList<>();
        //3、查出許可權列表迴圈放入  authorityList  中
        for (許可權實體類 url : 許可權集合) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(許可權的url);
            authorityList.add(simpleGrantedAuthority);
        }
        return authorityList;
    }
}
迷途者尋影而行