前后端分离权限设计方案:shiro+redis+jwt

1.设计方案说明 

        前后端分离项目,使用shiro进行权限校验,shiro相关组件本文不做说明,已经有很多文章说的很详细了.

        前后端分离的项目中传统的session方式会存在不一致的问题,所以就引入jwt,利用生成的token信息进行用户信息的认证与授权.后台接口中请求头通过传递token标识用户身份,token的有效存储使用缓存进行存储.

        用户认证与授权过程中会获取认证与授权信息,shiro支持进行缓存,这里使用第三方缓存,常用的有redis、ehcache,这里使用redis;

        自定义jwt过滤器,让每次请求都走自定义的校验逻辑,主要是重写executeLogin方法,每个请求都进行认证与授权.

2.shiro请求过程梳理(结合源码)

1.shiro请求路径图

对部分逻辑进行说明:

1.subject.login中获取用户认证登录信息:

        首先从缓存中取,如果缓存中获取不到,就去自定义的customRealm中进行登录认证(认证完成之后将认证信息添加到缓存中以便下次调用).AuthenticatingRealm.java中getAuthenticationInfo

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 从缓存中获取,如果获取不到就进行自定义的ream中进行登录认证
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached, perform the lookup:
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        if (info != null) {
            // 校验用户密码信息,此处有重写方法进行按照自定义的密码校验
            assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }

        return info;
    }
    
      // 从缓存中获取用户认证登录信息
    private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
        AuthenticationInfo info = null;

        Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
        if (cache != null && token != null) {
            log.trace("Attempting to retrieve the AuthenticationInfo from cache.");
            Object key = getAuthenticationCacheKey(token);
            info = cache.get(key);
            if (info == null) {
                log.trace("No AuthorizationInfo found in cache for key [{}]", key);
            } else {
                log.trace("Found cached AuthorizationInfo for key [{}]", key);
            }
        }

        return info;
    }
    
    // 走认证逻辑之后将认证信息添加到缓存中,以便下次请求调用
private void cacheAuthenticationInfoIfPossible(AuthenticationToken token, AuthenticationInfo info) {
        if (!isAuthenticationCachingEnabled(token, info)) {
            log.debug("AuthenticationInfo caching is disabled for info [{}].  Submitted token: [{}].", info, token);
            //return quietly, caching is disabled for this token/info pair:
            return;
        }

        Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
        if (cache != null) {
            Object key = getAuthenticationCacheKey(token);
            cache.put(key, info);
            log.trace("Cached AuthenticationInfo for continued authentication.  key=[{}], value=[{}].", key, info);
        }
    }

 2.授权信息验证:shiro拦截器中判断当前方法上是否有五种权限注解,如果有则进行权限校验,没有直接访问接口.AnnotationsAuthorizingMethodInterceptor.assertAuthorized中实现(此处注解都是基于对应的拦截器).

protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
        //default implementation just ensures no deny votes are cast:
         // 获取shiro默认支持的五种权限注解
        Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
        if (aamis != null && !aamis.isEmpty()) {
            for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
                   // 判断当前请求的方法上是否有上面五种权限注解,如果有则进行权限校验
                if (aami.supports(methodInvocation)) {
                    aami.assertAuthorized(methodInvocation);
                }
            }
        }
    }

3.权限校验方式:首先充缓存中获取授权信息,如果没有则进入到自定义的授权方法(授权完成之后将授权信息添加到缓存中以便下次调用)中,然后进行权限校验

    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
    // 获取用户授权信息    
    AuthorizationInfo info = getAuthorizationInfo(principals);
        return isPermitted(permission, info);
    }

    // 根据用户授权信息判断请求权限的具体实现
    protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
        Collection<Permission> perms = getPermissions(info);
        if (perms != null && !perms.isEmpty()) {
            for (Permission perm : perms) {
                if (perm.implies(permission)) {
                    return true;
                }
            }
        }
        return false;
    }

    // 获取授权信息
    protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {

        if (principals == null) {
            return null;
        }

        AuthorizationInfo info = null;

        if (log.isTraceEnabled()) {
            log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
        }
        // 首先从缓存中取
        Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
        if (cache != null) {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
            }
            Object key = getAuthorizationCacheKey(principals);
            info = cache.get(key);
            if (log.isTraceEnabled()) {
                if (info == null) {
                    log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
                } else {
                    log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
                }
            }
        }

        // 缓存中获取不到则从自定义的realm中进行授权
        if (info == null) {
            // Call template method if the info was not found in a cache
            info = doGetAuthorizationInfo(principals);
            // If the info is not null and the cache has been created, then cache the authorization info.
            if (info != null && cache != null) {
                if (log.isTraceEnabled()) {
                    log.trace("Caching authorization info for principals: [" + principals + "].");
                }
                Object key = getAuthorizationCacheKey(principals);
                // 授权信息添加到缓存中
                cache.put(key, info);
            }
        }

        return info;
    }

3.实现方案具体代码

redistemplate配置类:

package com.kawaxiaoyu.manage.management.common.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.kawaxiaoyu.manage.management.common.shiroRedis.IncludeShiroFields;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

/**
 * @ClassName: RedisTemplateConfig
 * @Desc: Redis Template 配置,用于序列化redis中对象问题
 * @Author: txm
 * @Date: 2021/5/21 10:24
 **/
@Component
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 自定义的string序列化器和fastjson序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 过滤未知字段
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        //只序列化必要shiro字段
        String [] needSerialize = {"realmPrincipals"};
        objectMapper.addMixIn(SimplePrincipalCollection.class, IncludeShiroFields.class);
        objectMapper.setFilters(new SimpleFilterProvider().addFilter("shiroFilter", SimpleBeanPropertyFilter.filterOutAllExcept(needSerialize)));
// 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);


        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);  // 设置缓存存储对象
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);  // 设置缓存存储对象
        redisTemplate.afterPropertiesSet();

        
        return redisTemplate;
    }
}

自定义realm:

package com.kawaxiaoyu.manage.management.common.shiro;

import com.kawaxiaoyu.manage.management.api.user.entity.ManageUser;
import com.kawaxiaoyu.manage.management.api.user.service.impl.ManageUserServiceImpl;
import com.kawaxiaoyu.manage.management.api.user.vo.LoginReqParam;
import com.kawaxiaoyu.manage.management.common.config.ManageConfig;
import com.kawaxiaoyu.manage.management.common.constant.RedisConstant;
import com.kawaxiaoyu.manage.management.common.excption.BussinessExcption;
import com.kawaxiaoyu.manage.management.common.shiroRedis.RedisCache;
import io.jsonwebtoken.Claims;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @ClassName: CustomRealm
 * @Desc: 自定义realm
 * @Author: txm
 * @Date: 2021/8/22 10:25
 **/
public class CustomRealm extends AuthorizingRealm {

    private static final Logger log= LoggerFactory.getLogger(CustomRealm.class);


    @Autowired
    private ManageUserServiceImpl manageUserService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ManageConfig manageConfig;


    public CustomRealm(){
        //这里使用我们自定义的Matcher验证接口
        this.setCredentialsMatcher(new JWTCredentialsMatcher());
        this.setAuthenticationCache(new RedisCache<>("authen"));
        this.setAuthorizationCache(new RedisCache<>("author"));
        // 设置对认证信息进行缓存
        this.setAuthenticationCachingEnabled(true);
    }


    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * shiro 身份验证
     * @param token
     * @return boolean
     * @throws AuthenticationException 抛出的异常将有统一的异常处理返回给前端
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {

        // 正常情况下doGetAuthenticationInfo方法只会执行一次,并且从redis中会获取到用户信息,此处登录业务正常情况下不会调用
        ManageUser manageUser = (ManageUser)redisTemplate.opsForValue().get(String.format(RedisConstant.MANAGELOGIN_USERID, ((JwtToken) token).getUserId()));
        if(manageUser == null){

            String tokenStr = ((JwtToken) token).getToken();
            if(StringUtils.isEmpty(tokenStr)) throw new BussinessExcption("传递token为空");

            Claims claims = JwtUtil.parseJWT(tokenStr, manageConfig.getTokenSecret());
            String username = claims.getSubject();
            String password = (String) claims.get("password");
            LoginReqParam loginReqParam = new LoginReqParam();
            loginReqParam.setUsername(username);
            loginReqParam.setPassword(password);
            manageUser  = manageUserService.login(loginReqParam);
        }

        // Object principal: 标识用户唯一的身份,比如说用户名或是邮箱, Object credentials---用户的安全凭证,比如说密码,String realmName-- 自定义realm
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                manageUser.getUserId(), manageUser.getPassword(), this.getName());


        return simpleAuthenticationInfo;
    }


    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = null;

        Long userId =(Long) principals.getPrimaryPrincipal();

        // 从缓存中获取用户角色以及权限信息
        String manageMenuKey = String.format(RedisConstant.MANAGEMENU_USERID, userId);
        List<String> manageMenuList = redisTemplate.opsForList().range(manageMenuKey, 0, -1);
        if(CollectionUtils.isEmpty(manageMenuList)){
            Set<String> menuSet = manageUserService.findMenuSet(userId);
            manageMenuList = menuSet.stream().collect(Collectors.toList());
            // 用户拥有角色信息添加到缓存中,设置默认值7天
            redisTemplate.opsForList().leftPushAll(manageMenuKey,manageMenuList);
            redisTemplate.expire(manageMenuKey, Duration.ofMillis(manageConfig.getTokenExpireTime()));
        }
        log.info("用户id:{},拥有的权限:{}", userId,manageMenuList);

        try {
            info = new SimpleAuthorizationInfo();
            //info.setRoles(roles);  // 放入角色信息
            info.setStringPermissions(manageMenuList.stream().collect(Collectors.toSet()));  // 放入权限信息
        }catch (Exception e){
            throw new AuthenticationException("授权失败!");
        }
        return info;
    }
}

        自定义用户密码校验

package com.kawaxiaoyu.manage.management.common.shiro;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;

/**
 * @ClassName: JWTCredentialsMatcher
 * @Desc: jwt验证匹配器
 * @Author: txm
 * @Date: 2021/8/22 10:28
 **/
public class JWTCredentialsMatcher implements CredentialsMatcher {

    /**
     * @Author: txm
     * @Description: 重写校验用户密码的逻辑
     * @Param: [authenticationToken, authenticationInfo]
     * @return: boolean
     * @Date:  2021/8/28 14:04
     **/
    @Override
    public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
        String token = (String) ((JwtToken)authenticationToken).getToken();
        String password = (String)authenticationInfo.getCredentials();
        //得到DefaultJwtParser
        Boolean verify = JwtUtil.isVerify(token, password);
        return verify;
    }
}

自定义jwt过滤器

package com.kawaxiaoyu.manage.management.common.shiro;

import com.alibaba.fastjson.JSONObject;
import com.kawaxiaoyu.manage.management.common.constant.Constants;
import com.kawaxiaoyu.manage.management.common.excption.BussinessExcption;
import com.kawaxiaoyu.manage.management.common.util.DateTimeUtil;
import io.jsonwebtoken.Claims;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;

/**
 * @ClassName: JwtFilter
 * @Desc: JwtFilter
 * @Author: txm
 * @Date: 2021/8/22 10:56
 **/
public class JwtFilter extends BasicHttpAuthenticationFilter {
    /**
     * 应用的HTTP方法列表配置基本身份验证筛选器。
     * 获取 request 请求 拒绝拦截登录请求
     * 执行登录认证方法
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)  {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();
        if (requestURI.equals(JwtUtil.loginPath) ) {
            return true;
        } else {
            try {
                boolean result = executeLogin(request, response);
                return result;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }



    /**
     * Authorization携带的参数为token
     * JwtToken实现了AuthenticationToken接口封装了token参数
     * 通过getSubject方法获取 subject对象
     * login()发送身份验证
     * @param request
     * @param response
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = httpServletRequest.getHeader("token");
            JwtToken jwtToken = new JwtToken(token);
            if(token != null){
            // 对传递过来的token进行解密,获取对象信息
            Claims claims = JwtUtil.parseJWT(token, JwtUtil.secretKey);
                String userId =(String) claims.get("userId");

            // 校验token是否有效
                boolean verifyToken = JwtUtil.verifyToken(token, Long.valueOf(userId));
                if(verifyToken) throw new BussinessExcption("用户认证信息失败:用户token身份过期,请重新登录");
                jwtToken.setUserId(Long.valueOf(userId));
            }
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            Subject subject = getSubject(request, response);
            subject.login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        } catch (Exception e) {
            // 过滤器异常返回前端处理方案
            JSONObject responseJSONObject = new JSONObject();
            responseJSONObject.put("success","false");
            responseJSONObject.put("msg",e.getMessage());
            responseJSONObject.put("time", DateTimeUtil.getCurrentTime());

            response.setCharacterEncoding(Constants.UTF8);
            response.setContentType(Constants.CONTENT_TYPE);
            OutputStream os = response.getOutputStream();
            os.write(JSONObject.toJSONBytes(responseJSONObject));
            os.flush();
            os.close();
        }
        return false;
    }




    /**
     * 对跨域提供支持
     */
    @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);
    }
}

自定义token

package com.kawaxiaoyu.manage.management.common.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @ClassName: JwtToken
 * @Desc: 自定义jwtToken
 * @Author: txm
 * @Date: 2021/8/22 10:33
 **/
public class JwtToken implements AuthenticationToken {
    private String token;

    private long userId;

    public long getUserId() {
        return userId;
    }

    public void setUserId(long userId) {
        this.userId = userId;
    }

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

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

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

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

自定义jwt工具类

package com.kawaxiaoyu.manage.management.common.shiro;

import com.kawaxiaoyu.manage.management.api.user.entity.ManageUser;
import com.kawaxiaoyu.manage.management.api.user.vo.LoginReqParam;
import com.kawaxiaoyu.manage.management.common.config.ManageConfig;
import com.kawaxiaoyu.manage.management.common.constant.RedisConstant;
import com.kawaxiaoyu.manage.management.common.excption.BussinessExcption;
import com.kawaxiaoyu.manage.management.common.util.PasswordUtil;
import com.kawaxiaoyu.manage.management.common.util.SpringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @ClassName: JwtUtil
 * @Desc: JwtUtil
 * @Author: txm
 * @Date: 2021/8/22 10:34
 **/
public class JwtUtil {

    public static final String secretKey;
    public static final String userSalt;
    public static final RedisTemplate redisTemplate;
    public static final long tokenExpireTime;
    public static final String loginPath;

    static {
        // 获取token加密签名
        ManageConfig manageConfig=SpringUtils.getBean("manageConfig");
        secretKey = manageConfig.getTokenSecret();
        userSalt = manageConfig.getUserSalt();
        tokenExpireTime = manageConfig.getTokenExpireTime();
        loginPath = manageConfig.getLoginPath();

        // 获取redisTemplate
         redisTemplate=SpringUtils.getBean("redisTemplate");

    }
    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法 私匙使用用户密码
     *
     * @param ttlMillis jwt过期时间
     * @param loginReqParam   登录成功的user对象
     * @return
     */
    public static String createJWT(long ttlMillis, LoginReqParam loginReqParam) {
        //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 创建私有声明
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("username", loginReqParam.getUsername());
        map.put("password", loginReqParam.getPassword());
        map.put("userId", String.valueOf(loginReqParam.getUserId()));

        //生成签发人
        String subject = loginReqParam.getUsername();


        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(map)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .setSubject(subject)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    /**
     * 校验token
     * 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过
     * @param token
     * @return
     */
    public static Boolean isVerify(String token, String password) {
        try {
            //得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    //设置签名的秘钥
                    .setSigningKey(secretKey)
                    //设置需要解析的jwt
                    .parseClaimsJws(token).getBody();

            String encryptPassword = PasswordUtil.encrypt((String) claims.get("password"), userSalt);
            if (encryptPassword.equals(password)) {
                return true;
            }
        } catch (Exception exception) {
            return false;
        }
        return false;

    }
    /**
     * Token的解密
     * @param token 加密后的token
     * @param secret 签名秘钥,和生成的签名的秘钥一模一样
     * @return
     */
    public static Claims parseJWT(String token, String secret) {
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(secret)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

    /**
     * Token有效性校验
     * @param token 加密后的token
     * @param userId 签名秘钥,和生成的签名的秘钥一模一样
     * @return
     */
    public static boolean verifyToken(String token,long  userId){
        boolean isExpired=false;
        // 校验token是否失效,从缓存中获取token信息
        String redisToken = (String) redisTemplate.opsForValue().get(String.format(RedisConstant.MANAGE_TOKEN, userId));
        if(StringUtils.isEmpty(redisToken) || !token.equals(redisToken)) {
            isExpired=true;
        }
        return isExpired;
    }

    
    /**
     * 获取用户信息
     * @param userId 签名秘钥,和生成的签名的秘钥一模一样
     * @return
     */
    public static ManageUser getUserInfo(long userId){
        ManageUser manageUser = (ManageUser) redisTemplate.opsForValue().get(String.format(RedisConstant.MANAGELOGIN_USERID, userId));
        if(manageUser == null) throw new BussinessExcption("用户信息获取失败,请重新登录");
        return manageUser;
    }

}

自定义shiro配置类:

package com.kawaxiaoyu.manage.management.common.shiro;

import com.kawaxiaoyu.manage.management.common.shiroRedis.RedisCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
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;

/**
 * @ClassName: ShiroConfiguration
 * @Desc: shiro配置类
 * @Author: txm
 * @Date: 2021/8/22 10:49
 **/
@Configuration
public class ShiroConfiguration {

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/user/login", "anon");
        // 对静态资源设置匿名访问
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/html/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/ajax/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        // knife4j权限放开
//        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/doc.html", "anon");

        //swagger接口权限 开放
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/configuration/**", "anon");
        filterChainDefinitionMap.put("/service-worker.js", "anon");
        filterChainDefinitionMap.put("/index.html", "anon");
        filterChainDefinitionMap.put("/manifest.json", "anon");
        filterChainDefinitionMap.put("/robots.txt", "anon");
        filterChainDefinitionMap.put("/precache-manifest.eea302037a9c2783bdf341d6c2dd2ca2.js", "anon");
//        filterChainDefinitionMap.put("/user/logout", "anon");
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边,不使用自定义的jwt过滤器
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }


    @Bean("securityManager")
    public SecurityManager securityManager(CustomRealm customRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm);
        /*
         * 关闭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);

        // 注入缓存管理器;不使用ehcache
//        securityManager.setCacheManager(getEhCacheManager());

        //  缓存使用redis
        securityManager.setCacheManager(new RedisCacheManager());

        return securityManager;
    }


    /**
     * 创建自定义的CustomRealm @bean
     */
    @Bean("customRealm")
    public CustomRealm shiroRealm() {
        CustomRealm customRealm = new CustomRealm();
        // 使用redis进行缓存认证以及授权信息
        // 开启全局缓存
        customRealm.setCachingEnabled(true);
        // 开启认证缓存
        customRealm.setAuthenticationCachingEnabled(true);
        // 开启授权缓存管理
        customRealm.setAuthorizationCachingEnabled(true);
        // 开启Redis缓存
        customRealm.setCacheManager(new RedisCacheManager());
        return customRealm;
    }


    //自动创建代理,没有这个鉴权可能会出错
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }
    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

自定义shiro工具类:

package com.kawaxiaoyu.manage.management.common.shiro;

import com.kawaxiaoyu.manage.management.api.user.entity.ManageUser;
import com.kawaxiaoyu.manage.management.common.util.CheckEmptyUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.BeanUtils;

/**
 * @ClassName: ShiroUtils
 * @Desc: shiro工具类
 * @Author: txm
 * @Date: 2021/6/23 16:42
 **/
public class ShiroUtils
{
    public static Subject getSubject()
    {
        return SecurityUtils.getSubject();
    }


    public static ManageUser getSysUser()  {
        ManageUser user = null;
        Object obj = getSubject().getPrincipal();
        if (CheckEmptyUtil.isNotNull(obj))
        {
            user = new ManageUser();
            BeanUtils.copyProperties(obj,user);
        }
        return user;
    }


    public static Long getUserId()
    {
        return getSysUser().getUserId().longValue();
    }

    public static String getLoginName()
    {
        return getSysUser().getLoginName();
    }

    public static String getIp()
    {
        return getSubject().getSession().getHost();
    }


}

4.反思与总结

        问题主要出现在过滤器中出现的异常如何进行返回到前端(不像controller层抛出异常能使用使),这里提供的方案是流的方式组装数据返回.

        对于shiro接入第三方缓存用时较多,通过本次可以体会一下经典框架如何进行自定义接入,实现原理都大概相同.

        认证信息进行反序列化的地方出现过问题卡了很长时间,Jackson支持对指定的属性进行过滤,设置方式可以参考Redistemplate配置类.

        关于实现的细节问题,在代码中已做补充说明!如有疑问或不当之处欢迎评论区留言.

  

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
可以的,我可以为您提供一个基于Spring Boot、JWTShiroRedis的例子。这个例子将演示如何使用这些技术实现用户认证和授权,并且将用户的状态存储在Redis中。 首先,您需要创建一个Spring Boot项目并添加所需的依赖。 在pom.xml文件中添加以下依赖: ``` <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.7.1</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> ``` 接下来,创建一个名为`JwtUtils`的JWT工具类,用于生成和验证JWT令牌。您可以参考以下代码: ```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Date; @Component public class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private int expiration; @PostConstruct public void init() { secret = Base64.getEncoder().encodeToString(secret.getBytes()); } public String generateToken(String username) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration * 1000); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } } ``` 然后,创建一个名为`RedisUtils`的Redis工具类,用于操作Redis。您可以参考以下代码: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedisUtils { @Autowired private RedisTemplate<String, Object> redisTemplate; public void set(String key, Object value, long expiration) { redisTemplate.opsForValue().set(key, value, expiration, TimeUnit.SECONDS); } public Object get(String key) { return redisTemplate.opsForValue().get(key); } public void delete(String key) { redisTemplate.delete(key); } public boolean hasKey(String key) { return redisTemplate.hasKey(key); } } ``` 接下来,创建一个名为`JwtRealm`的Shiro Realm,用于验证JWT令牌和授权。您可以参考以下代码: ```java import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class JwtRealm extends AuthorizingRealm { @Autowired private JwtUtils jwtUtils; @Autowired private RedisUtils redisUtils; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // TODO: 实现授权逻辑 } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { JwtToken jwtToken = (JwtToken) token; String username = jwtUtils.getUsernameFromToken(jwtToken.getToken()); if (username == null || !jwtUtils.validateToken(jwtToken.getToken())) { throw new AuthenticationException("无效的令牌"); } // TODO: 查询用户信息并返回认证信息 return new SimpleAuthenticationInfo(username, jwtToken.getToken(), getName()); } } ``` 最后,创建一个名为`JwtToken`的Shiro Token,用于封装JWT令牌。您可以参考以下代码: ```java import org.apache.shiro.authc.AuthenticationToken; 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; } } ``` 以上是一个基于Spring Boot、JWTShiroRedis的例子。您可以根据您的需求进行修改和扩展。希望对您有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卖柴火的小伙子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值