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;


    /**
     簽發對象:隨意
     簽發時間:現在
     有效時間:30分鍾
     載荷內容:自定義內容
     加密密鑰:鹽 + 密鑰
     */
    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;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM