1. 程式人生 > 實用技巧 >Shiro-JWT SpringBoot前後端分離許可權認證的一種思路

Shiro-JWT SpringBoot前後端分離許可權認證的一種思路

JWT-Shiro 整合

JWT-與Shiro整合進行授權認證的大致思路 圖示

大致思路

  1. 將登入驗證從shiro中分離,自己結合JWT實現
  2. 使用者登陸後請求認證伺服器進行密碼等身份資訊確認,確認成功後 封裝相關使用者資訊 生成token 相應給前端.
  3. 之後每次訪問資源介面都在請求頭中攜帶認證時生成的token
  4. 當發起資源請求時首先請求被請求過濾器攔截,攔截後判斷請求頭中是否含有token
  5. 如果含有token對token進行認證認證成功後對token進行解析,之後進行授權,擁有許可權則進行放行
  6. 反之返回相關錯誤資訊

核心點

  • token相關工具類的封裝
  • 自定義重寫shiro過濾器 extends AccessControlFilter
  • 自定義實現shiro Realm extends AuthorizingRealm
  • 實現自定義的shiroToken implements AuthenticationToken

具體程式碼

生成token的工具類

public class JWTUtils {
    //金鑰用於生成token的簽名
    private static final String SIGN = "!1qaz.(";

    /**
     * 生成token
     */
    public static String getToken(String userId,String userName, String roles, String permissions) {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE, 7);
        JWTCreator.Builder builder = JWT.create()
                .withIssuer("HuangShen")//token簽發者
                .withExpiresAt(instance.getTime()) //過期時間
                .withClaim("userId", userId)//相關資訊
                .withClaim("userName", userName)
                .withClaim("roles", roles)
                .withClaim("permissions", permissions);

        //使用HMAC256演算法生成token
        String token = builder.sign(Algorithm.HMAC256(SIGN)); 
        return token;
    }

    /**
     * 解碼token
     *
     * @param token token 
     */
    public static DecodedJWT verify(String token){
        DecodedJWT verify =JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
        return verify;
    }

自定義重寫shiro過濾器

public class JWTFilter extends AccessControlFilter {

    /**
     * 此方法首先執行當此方法返回false時繼續執行onAccessDenied方法
     * 返回true允許訪問
     * 返回 false拒絕訪問
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
           
        //獲取主體物件
        Subject subject = SecurityUtils.getSubject();
        System.out.println("===允許訪問===");
        //當主體物件不為空且已經獲得認證時允許訪問 
        if (null != subject && subject.isAuthenticated()){
            return true;
        }
            return false;
    }


    /**
     * 當isAccessAllowed返回值為false時執行
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String token = httpServletRequest.getHeader("token");
        //客戶端沒有攜帶token
        if (StringUtils.isEmpty(token)) {
            System.out.println("請求頭沒有token");
            return true;
        }
        System.out.println("拒接訪問");
        JWTToken jwtToken = new JWTToken(token);
        Subject subject = SecurityUtils.getSubject();
        //進行認證
        subject.login(jwtToken);
        return true;
    }

自定義實現shiro Realm

/**
 * 繼承AuthorizingRealm類重寫doGetAuthorizationInfo(授權)
 * doGetAuthenticationInfo(認證)
 */
public class CustomerRealm extends AuthorizingRealm {

    /**
     * 認證
     * @param authenticationToken  認證token
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //獲取主體資訊
        JWTToken principal = (JWTToken) authenticationToken;
        DecodedJWT verify;
        //建立自定義principal並賦值
        TokenPayload tokenPayload = new TokenPayload();
        //解析token
        try {
            verify = JWTUtils.verify((String) principal.getPrincipal());
            tokenPayload.setUserId(verify.getClaim("userId").asString());
            tokenPayload.setRoles(verify.getClaim("roles").asString());
            tokenPayload.setUserName(verify.getClaim("userName").asString());
            tokenPayload.setPermissions(verify.getClaim("permissions").asString());
        } catch (AlgorithmMismatchException exception) {
            throw new AuthenticationException("演算法不匹配異常" + exception.getMessage());
        } catch (SignatureVerificationException exception) {
            throw new AuthenticationException("簽名驗證異常" + exception.getMessage());
        } catch (TokenExpiredException exception) {
            throw new AuthenticationException("token過期異常" + exception.getMessage());
        } catch (InvalidClaimException exception) {
            throw new AuthenticationException("無效Claim異常" + exception.getMessage());
        } catch (JWTDecodeException exception) {
            throw new AuthenticationException("JWT解碼異常" + exception.getMessage());
        } catch (JWTVerificationException exception) {
            throw new AuthenticationException("JWT驗證異常" + exception.getMessage());
        } catch (RuntimeException exception) {
            throw new RuntimeException(exception.getMessage());
        }


        System.out.println("認證完成");
        //將token解析過後的資訊封裝成為主體傳入 授權時使用
        return new SimpleAuthenticationInfo(tokenPayload, true, this.getName());
    }

    /**
     * 授權
     * @param principalCollection 授權主體
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("開始授權");
        TokenPayload primaryPrincipal = (TokenPayload) principalCollection.getPrimaryPrincipal();

        System.out.println(primaryPrincipal.getRoles());
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //新增角色資訊
        simpleAuthorizationInfo.addRole(primaryPrincipal.getRoles());
        //新增許可權資訊
        simpleAuthorizationInfo.addStringPermission(primaryPrincipal.getPermissions());


        System.out.println("授權完成");
        return simpleAuthorizationInfo;
    }

    @Override
    public Class<?> getAuthenticationTokenClass() {
        return JWTToken.class;
    }

實現自定義的shiroToken

/**
 * 為了便於使用由JWT生成的token 自定義實現自己的token
 */
public class JWTToken implements AuthenticationToken {

    //儲存由請求頭中獲取的token
    private final String jwtToken;

    public JWTToken(String jwtToken) {
        this.jwtToken = jwtToken;
    }

    @Override
    public Object getPrincipal() {
        return this.jwtToken;
    }

    @Override
    public Object getCredentials() {
        return true;
    }
}

shiro配置

@Configuration
public class ShiroConfig {

    /**
     * 1.建立shiroFilterFactoryBean
     * 負責攔截多有請求
     *
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //給filter設定安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwtfilter",new JWTFilter());
        //將自定義過濾器加入到shiro 過濾其中
        shiroFilterFactoryBean.setFilters(filters);

        // 完全無狀態認證  noSessionCreation 不保留每次會話的session
        //因此每次請求都會進行授權和認證
        HashMap<String, String> map = new HashMap<>();
        map.put("/shiro/login","anon");
        map.put("/shiro/**","noSessionCreation,jwtfilter");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        shiroFilterFactoryBean.setLoginUrl("/shiro/unauthorized");
        return shiroFilterFactoryBean;
    }

    /**
     * 2.建立安全管理器
     *
     * @param realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     * 3.自定義Realm
     *
     * CredentialsMatcher 證書匹配器
     * @return 自定義Realm
     */
    @Bean("myRealm")
    public Realm getRealm() {
        CustomerRealm customerRealm = new CustomerRealm();
        
        
        return customerRealm;
    }

總結

使用JWTToken與shiro進無狀態授權認證時 實際上登入時放棄的使用shiro的認證 登陸時使用自己實現的登入方法並且生成Token,無狀態會話,用於shiro不儲存每次會話的session 因此每次請求都會進行一次完整的shiro授權認證流程 ,可以使用Redis 等其他快取的方式實現shiro的快取 減小系統壓力。