zuul集成jwt和shiro進行認證和鑑權

廢話不多說,直接上代碼。

首先,我將shiro鑑權和jwt認證做成了一個微服務:lls-base-shiro。

關鍵pom如下:

     <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro-spring-boot-web-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--JWT-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${java-jwt.version}</version>
        </dependency>

shiroconfig如下:

package com.lenovoedu.config;

import com.lenovoedu.interceptor.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


/**
 * 項目名稱:lls2.0
 * 類名稱:ShiroConfig
 * 類描述:shiro配置類
 * 創建人:YuanGL
 * 創建時間:2019年4月1日14:12:36
 * version 2.0
 */
@Configuration
public class ShiroConfig {
    /**
     * 注入安全過濾器
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // setLoginUrl 如果不設置值,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/notLogin");
        // 設置無權限時跳轉的 url;
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
        //攔截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不會被攔截的鏈接 順序判斷
//        filterChainDefinitionMap.put("/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        //前後端帶login登錄的或者其他登錄的通通放行
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
        filterChainDefinitionMap.put("/loginjwt", "anon");
        filterChainDefinitionMap.put("/websocket/**", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        // 添加自己的過濾器並且取名爲jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最爲下邊
        filterChainDefinitionMap.put("/**", "jwt");
        //未授權界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 注入安全管理器
     * @param myRealm
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(ShiroRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);

        /*
         * 關閉shiro自帶的session,詳情見文檔
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 自定義身份認證 realm;
     * <p>
     * 必須寫這個類,並加上 @Bean 註解,目的是注入 CustomRealm,
     * 否則會影響 CustomRealm類 中其他類的依賴注入
     */
    @Bean
    public ShiroRealm shiroRealm() {
        return new ShiroRealm();
    }

    /**
     * LifecycleBeanPostProcessor將Initializable和Destroyable的實現類統一在其內部
     * 自動分別調用了Initializable.init()和Destroyable.destroy()方法,從而達到管理shiro bean生命週期的目的。
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

}

ShiroRealm如下:

package com.lenovoedu.config;

import com.lenovoedu.common.JwtToken;
import com.lenovoedu.model.sys.model.SysUser;
import com.lenovoedu.shiro.service.IPermitService;
import com.lenovoedu.utils.JWTGenerator;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Set;

/**
 * 項目名稱:lls2.0
 * 類名稱:TraxexShiroRealm
 * 類描述:shiro AuthorizingRealm繼承實現
 * 創建人:YuanGL
 * 創建時間:2019年4月1日16:17:46
 * version 2.0
 */
public class ShiroRealm extends AuthorizingRealm {

    private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);

    @Autowired
    private IPermitService permitService;
    @Autowired
    private JWTGenerator jwtGenerator;

    /**
     * 必須重寫此方法,不然Shiro會報錯
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授權
     * @param principals 用戶信息
     * @return AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        String email = JWTGenerator.getEmail(principals.toString());
        Set<String> roles = permitService.getSysUserRoles(email);
        Set<String> permits =  permitService.getSysUserAuthCodes(email);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles);
        info.setStringPermissions(permits);
        return info;
    }

    /**
     * 默認使用此方法進行用戶名正確與否驗證,錯誤拋出異常即可。
     * @param auth 用戶權限信息
     * @return AuthenticationInfo
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = (String) auth.getCredentials();
        String email = JWTGenerator.getEmail(token);
        if (email == null) {
            throw new AuthenticationException("token無效");
        }
        SysUser sysUser = permitService.getUserByLoginName(email);
        if(sysUser==null){
            throw new AuthenticationException("用戶不存在!");
        }
        // 檢查用戶狀態
        if(!sysUser.getIsAble().equals("Y")) {
            throw new AuthenticationException("用戶已刪除!");
        }
        if(!jwtGenerator.requireTokenBoolean(request,sysUser.getPassword())){
            throw new AuthenticationException("用戶名或密碼錯誤(token無效或者與登錄者不匹配)!");
        }
        return new SimpleAuthenticationInfo(token, token, "shiro_realm");
    }
}

JwtToken如下:

package com.lenovoedu.common;


import org.apache.shiro.authc.AuthenticationToken;

/**
 * JwtToken:實現shiro的AuthenticationToken接口的類JwtToken
 *
 * @author zhangxiaoxiang
 * @date: 2019/07/12
 */
public class JwtToken implements AuthenticationToken{

    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

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

JwtFilter如下:(jwt的攔截器)

package com.lenovoedu.interceptor;

import com.lenovoedu.common.JwtToken;
import com.lenovoedu.constants.BackendYMLConstants;
import com.lenovoedu.utils.StringPool;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.Filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * JwtFilter:jwt過濾器來作爲shiro的過濾器
 * 方法執行順序分別爲:isAccessAllowed(裏面的isLoginRequest實現了isLoginAttempt),isLoginAttempt,executeLogin。
 * 驗證不成功會執行onAccessDenied
 *
 * @author ygl
 * @date: 2019/07/12
 */
@Component//這個注入與否影響不大
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {

    /**
     * 處理未經驗證的請求
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
            throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setStatus(401);
        return false;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        // 原用來判斷是否是登錄請求,在本例中不會攔截登錄請求,用來檢測Header中是否包含 JWT token 字段
        if (this.isLoginRequest(request, response)){
            return false;
        }
        boolean allowed = false;
        try {
            // 檢測Header裏的 JWT token內容是否正確,嘗試使用 token進行登錄
            allowed = executeLogin(request, response);
        } catch (IllegalStateException e) { // not found any token
        } catch (Exception e) {
        }
        return allowed || super.isPermissive(mappedValue);
    }

    /**
     * 判斷用戶是否想要登入。
     * 檢測 header 裏面是否包含 Token 字段
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(StringPool.HEADER_TOKEN);
        return token == null;
    }

    /**
     * 執行登錄
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(BackendYMLConstants.HEADER_TOKEN);
        if (null == token) {
            String msg = "executeLogin method token must not be null";
            throw new IllegalStateException(msg);
        }
        JwtToken jwtToken = new JwtToken(token);
        // 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
        try {
//            這個方法會執行到realm的認證方法
            this.getSubject(request, response).login(jwtToken);
            // 如果沒有拋出異常則代表登入成功,返回true
            return true;
        } catch (AuthenticationException e) {
            return false;
        }

    }
    /**
     * 對跨域提供支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域時會首先發送一個option請求,這裏我們給option請求直接返回正常狀態
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

Jwt的工具類如下:

package com.lenovoedu.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.lenovoedu.base.log.Logger;
import com.lenovoedu.base.log.LoggerFactory;
import com.lenovoedu.constants.BackendYMLConstants;
import com.lenovoedu.exception.DescribeException;
import com.lenovoedu.exception.ExceptionEnum;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JWTGenerator {

    private static Logger logger = LoggerFactory.getLogger(JWTGenerator.class);

    private final static Map<String, Object> JWT_HEADER = new HashMap<String, Object>(){
        {
            put("alg", "HmacSHA256");
            put("typ", "JWT");
        }
    };

    public final static String USER_ID = "userId";
    public final static String EMAIL = "email";

    @Value(BackendYMLConstants.JWT_TOKEN_SECRET)
    public String jwtTokenSecret;
    @Value(BackendYMLConstants.JWT_AUDIENCE)
    public String jwtAudience;
    @Value(BackendYMLConstants.JWT_ISS_USER)
    public String jwtIssUser;
    @Value(BackendYMLConstants.JWT_SUBJECT)
    public String jwtSubject;
    @Value(BackendYMLConstants.JWT_EXPIRES_TIME)
    public Long tokenExpireTime;

    public String createToken(HttpServletRequest request, String email,String password) throws UnsupportedEncodingException {
        String ua = request.getHeader(StringPool.USER_AGENT);
        if (StringUtils.isNotEmpty(ua)) {
            return JWT.create()
                    .withHeader(JWT_HEADER)
                    .withClaim(EMAIL, email)
                    .withExpiresAt(new Date(System.currentTimeMillis() + tokenExpireTime))
                    .withNotBefore(new Date())
                    .withIssuer(jwtIssUser)
                    .withSubject(jwtSubject)
                    .withAudience(jwtAudience)
                    .sign(Algorithm.HMAC256(password + ua));

        }
        return null;
    }

    public DecodedJWT requireToken(HttpServletRequest request,String password) throws UnsupportedEncodingException {
        String ua = request.getHeader(StringPool.USER_AGENT);
        String token = request.getHeader(StringPool.HEADER_TOKEN);
        logger.info("request token:{}", token);
        if (StringUtils.isNotBlank(ua)
                && StringUtils.isNotBlank(token)) {
            Algorithm algorithm = Algorithm.HMAC256(password + ua);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(jwtIssUser)
                    .withSubject(jwtSubject)
                    .withAudience(jwtAudience)
                    .acceptExpiresAt(0)
                    .acceptNotBefore(0)
                    .build(); //Reusable verifier instance
            return verifier.verify(token);
        }
        throw new DescribeException(ExceptionEnum.NEED_LOGIN_ERROR);
    }

    public boolean requireTokenBoolean(HttpServletRequest request,String password){
        try {
            String ua = request.getHeader(StringPool.USER_AGENT);
            String token = request.getHeader(StringPool.HEADER_TOKEN);
            logger.info("request token:{}", token);
            if (StringUtils.isNotBlank(ua)
                    && StringUtils.isNotBlank(token)) {
                Algorithm algorithm = null;
                    algorithm = Algorithm.HMAC256(password + ua);
                JWTVerifier verifier = JWT.require(algorithm)
                        .withIssuer(jwtIssUser)
                        .withSubject(jwtSubject)
                        .withAudience(jwtAudience)
                        .acceptExpiresAt(0)
                        .acceptNotBefore(0)
                        .build(); //Reusable verifier instance
                DecodedJWT verify = verifier.verify(token);
                return true;
            }
            return false;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     *
     * @return token中包含的用戶信息------》email
     */
    public static String getEmail(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(EMAIL).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

配置zuul的前置過濾器如下:

package com.lenovoedu.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 第二個前置過濾器,用來判斷是否需要繼續轉發
 * @author ygl
 * @date: 2020年3月22日23:45:56
 */
@Component
public class SecondFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(SecondFilter.class);

    @Override
    public String filterType() {
        /*
        pre:可以在請求被路由之前調用
        route:在路由請求時候被調用
        post:在route和error過濾器之後被調用
        error:處理請求時發生錯誤時被調用
        * */
        // 前置過濾器
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        //// 優先級爲0,數字越大,優先級越低
        return -1;
    }
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        return true;
    }

    @Override
    public Object run() {
        logger.debug("*****************FirstFilter run start*****************");
        System.out.println("*****************FirstFilter run start*****************");
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        HttpServletResponse response = ctx.getResponse();
        response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials","true");
        response.setHeader("Access-Control-Allow-Headers","x-requested-with, authorization, content-type");
        response.setHeader("Access-Control-Allow-Methods","POST,GET");
        response.setHeader("Access-Control-Expose-Headers","X-forwared-port, X-forwarded-host, Content-Disposition");
        response.setHeader("Vary","Origin,Access-Control-Request-Method,Access-Control-Request-Headers");
        String ua = request.getHeader("User-Agent");
        String token = request.getHeader("token");
        String url = request.getRequestURL().toString();
        logger.info("request token:{}", token);
        if ((StringUtils.isNotBlank(ua) && StringUtils.isNotBlank(token)) || url.contains("loginjwt") || url.contains("swagger")|| url.contains("v2") || url.contains("websocket")) {
            //允許繼續路由
            ctx.addZuulRequestHeader("token", token);
            ctx.addZuulRequestHeader("User-Agent", ua);
            ctx.setSendZuulResponse(true);
            ctx.setResponseStatusCode(200);
        }else{
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
        }
        logger.debug("*****************FirstFilter run end*****************");
        return null;
    }
}

這樣就已經實現了jwt的認證和鑑權。鑑權的話,只需要在相應的方法上添加@RequiresPermissions或者@RequiresRole註解即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章