Shiro&Jwt驗證


此篇基於 SpringBoot 整合 Shiro & Jwt 進行鑒權 相關代碼編寫與解析

首先我們創建 JwtFilter 類 繼承自 BasicHttpAuthenticationFilter

org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

此類是個過濾器,后期配置Shiro時會引用到

重寫4個重要的方法 其執行順序亦是如下

1. preHandle(..)  我理解為是前置處理
2. isAccessAllowed(..) 請求是否被允許
3. isLoginAttempt(..) 是否是嘗試登陸,去查實[請求頭]里是否包含[Authorization]
4. executeLogin(..) 執行登陸操作 其會調用getSubject(request, response).login(jwtToken)進行登陸驗權

preHandle 方法

可以理解為前置處理,我們在這進行一些跨域的必要設置

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"));
//跨域請求會發送兩次請求首次為預檢請求,其請求方法為 OPTIONS
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
     httpServletResponse.setStatus(HttpStatus.OK.value());
     return false;
}
return super.preHandle(request, response);

isAccessAllowed 方法

其實這個方法我們會手動調用 isLoginAttempt 方法及 executeLogin 方法
isLoginAttempt判斷用戶是否想嘗試登陸,判斷依據為請求頭中是否包含 Authorization 授權信息,也就是 Token 令牌
如果有則再執行executeLogin方法進行登陸驗證操作,就是我們整合后的鑒權操作,因為用Token拋開了Session,此處就相當於是否存在Session的操作,存在則表明登陸成功,不存在則需要登陸操作,或者Session過期需要重新登陸是一個原理性質,此方法在這里是驗證JwtToken是否合法,不合法則返回401需要重新登陸

不合法的原因大致一這些

  • Token不正確 可以是被篡改 不能解析
  • Token過期
  • Token已被注銷(需自己去實現)
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        if (this.isLoginAttempt(request, response)) {

            // 進行驗證登陸JWT 可以trycatch下可以捕獲Token過期異常等信息
            this.executeLogin(request, response);
            
        } else {
            // 沒有攜帶Token
            HttpServletRequest httpRequest = (HttpServletRequest)request;
            String httpMethod = httpRequest.getMethod();
            String requestURI = httpRequest.getRequestURI();
            logger.info("當前請求 {} Authorization屬性(Token)為空 請求類型 {}", requestURI, httpMethod);
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                httpServletResponse.setCharacterEncoding("UTF-8");
                httpServletResponse.setContentType("application/json; charset=utf-8");
                try (PrintWriter out = httpServletResponse.getWriter()) {

                    out.append("用戶認證失敗" + msg);
                } catch (IOException e) {
                    //logger.error("直接返回Response信息出現IOException異常", e);
                }
            return false;
        }
        return true;
    }

isLoginAttempt 方法 是否嘗試登陸

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String token = this.getAuthzHeader(request);
        return token != null;
    }

executeLogin 方法

執行登陸操作 其實就是對 Token 進行驗證操作,這里我們需要另外一個類去處理 Token 驗證 (MyRealm 類的doGetAuthenticationInfo方法)

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        String token = this.getAuthzHeader(request);
        //這里需要自己實現對Token驗證操作
        JwtToken jwtToken = new JwtToken(token);
        getSubject(request, response).login(jwtToken);//如果登陸失敗會拋出異常(Token鑒權失敗)
        return true;
    }

創建 JwtToken 類 繼承自AuthenticationToken

org.apache.shiro.authc.AuthenticationToken

這里本來是存取用戶名及密碼的字段用來登陸,現在因為是Token不存在需要攜帶用戶名 所以把字段都設置成Token

public class JwtToken implements AuthenticationToken {
    private static final long serialVersionUID = -634556778977L;
    
    private String token;

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

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

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

JwtConfig類的創建

這個類用於創建 Token 及解碼 Token 里的信息

@ConfigurationProperties(prefix = "config.jwt")
@Component
public class JwtConfig {

    private String secret;
    private long expire;
    private String header;

    /**
     * 生成token
     *
     * @param subject
     * @return
     */
    public String createToken(String subject) {
        Date nowDate = new Date();
        //過期時間 默認單位 (天)
        Date expireDate = new Date(nowDate.getTime() + expire * 1000 * 60 * 60 * 24);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 獲取token中注冊信息
     *
     * @param token
     * @return
     */
    public Claims getTokenClaim(String token) {
        try {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 獲取token 解析信息
     * @param token
     * @return
     */
    public String getTokenInfo(String token){
        Claims claims=getTokenClaim(token);
        if(claims==null){
            throw new RuntimeException("Token 信息異常");
        }
        String tokenInfo=claims.getSubject();
        if(StringUtils.isBlank(tokenInfo)){
            throw new RuntimeException("Token 信息異常 解析值為空");
        }
        return tokenInfo;
    }

    /**
     * 驗證token是否過期失效
     *
     * @param expirationTime
     * @return
     */
    public boolean isTokenExpired(Date expirationTime) {
        return expirationTime.before(new Date());
    }

    /**
     * 獲取token失效時間
     *
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token) {
        return getTokenClaim(token).getExpiration();
    }

    /**
     * 獲取用戶名從token中
     */
    public String getUsernameFromToken(String token) {
        return getTokenClaim(token).getSubject();
    }

    /**
     * 獲取jwt發布時間
     */
    public Date getIssuedAtDateFromToken(String token) {
        return getTokenClaim(token).getIssuedAt();
    }


    ...set get
}

配置文件信息

config.jwt.secret=123!@#
config.jwt.expire=15
config.jwt.header=Authorization

實現自己的Realm 創建MyRealm類 繼承自AuthorizingRealm

import io.jsonwebtoken.Claims;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;


/**
 * @author zy
 */
@Component
public class MyRealm extends AuthorizingRealm {

    private static Logger logger = LogManager.getLogger(MyRealm.class);

    /**
    *   這里需要實現自己的用戶登陸驗證信息及 數據權限相關的信息獲取
    */
    @Resource
    private UserService userService;

    @Resource
    private JwtConfig jwtConfig;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 只有當需要檢測用戶權限的時候才會調用此方法,例如checkRole,checkPermission之類的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("====================數據權限認證====================");
        String username = jwtConfig.getTokenInfo(principals.toString());
        UserInfo user = userService.getUserAndRole(username);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        List<Role> roles = user.getRoles();

        if (roles.isEmpty()) {
            logger.warn("該用戶 {} 沒有角色,默認賦予user角色", username);
            Role r = new Role();
            r.setRole("user");
            roles.add(r);
        }

        /**
        *    這里是我自己實現的數據權限認證可做參考用
        */
        Set<String> permissionSet = new HashSet<>(roles.size() * 16);
        for (Role role : roles) {
            List<Permission> temList = role.getPermissions();
            if (temList == null || temList.isEmpty()) {
                logger.warn("該角色 {} 沒有賦予相應權限信息", role.getRole());
                continue;
            }
            for (Permission tem : temList) {
                if (tem.getId().indexOf(":") > -1) {
                    permissionSet.add(tem.getId());
                }
            }
        }

        simpleAuthorizationInfo.setRoles(roles.stream().map(Role::getRole).collect(Collectors.toSet()));
        simpleAuthorizationInfo.setStringPermissions(permissionSet);
        return simpleAuthorizationInfo;
    }

    /**
     * 默認使用此方法進行用戶名正確與否驗證,錯誤拋出異常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) {
        logger.info("====================Token認證====================");
        String token = auth.getCredentials().toString();
        Claims claims = jwtConfig.getTokenClaim(token);
        if (claims == null) {
            throw new AuthenticationException("解析Token異常");
        }
        if (jwtConfig.isTokenExpired(claims.getExpiration())) {
            throw new AuthenticationException("Token過期");
        }
        String username = claims.getSubject();
        if (username == null || username == "") {
            logger.error("Token中帳號為空");
            throw new AuthenticationException("Token中帳號為空");
        }
        UserInfo user = userService.getUserByName(username);
        if (user == null) {
            throw new AuthenticationException("該帳號不存在");
        }
        DataContextHolder.setCurrentUser(user);
        return new SimpleAuthenticationInfo(token, token, getName());
    }
}

配置Shiro 創建ShiroConfig類

LifecycleBeanPostProcessor這個類並不一定要手動創建,手動創建可能存在一些問題。我遇見的坑就在這里。至於原因希望大家不吝賜教

import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

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

/**
 * @author zy
 */
@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm myRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm);

        // 關閉Shiro自帶的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        // 設置自定義Cache緩存 根據項目情況而設置
        manager.setCacheManager(new CustomCacheManager());
        return manager;
    }

    /**
     * 添加自己的過濾器,自定義url規則
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager, RedisTemplate<String, String> redisTemplate, JwtConfig jwtConfig) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        //配置過濾器 對 『anon』不進行攔截
        Map<String, Filter> filterMap = new HashMap<>(3);
        filterMap.put("anon", new AnonymousFilter());
        filterMap.put("jwt", new JwtFilter(redisTemplate, jwtConfig));

        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);


        factoryBean.setLoginUrl("/login");

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(16);

        // 配置不過濾
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/testpage", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login/**", "anon");
        filterChainDefinitionMap.put("/unauthorized/**", "anon");
        // swagger
        filterChainDefinitionMap.put("/swagger**/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");

        filterChainDefinitionMap.put("/**", "jwt");

        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return factoryBean;
    }

//    @Bean
//    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
//        return new LifecycleBeanPostProcessor();
//    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}


免責聲明!

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



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