引入shiro-redis包依赖+jwt 配置流程及代码实现

引入shiro-redis包依赖+jwt 配置流程及代码实现

路线

导入shiro-redis 依赖之后,就需要根据官方指示去实现两个bean的定义

1、在shiroConifg中定义这两个方法SessionManager and SessionsSecurityManager

2、在注入这两个注解redisSessionDAO and redisCacheManager,并重写上面两个方法

3、我们后端接收的请求都需要走jwtFilter,所以需要在shiroConifg中定义两个跟jwtFilter有关的方法,ShiroFilterChainDefinitionShiroFilterFactory

4、在shiroFilterFactoryBean方法中需要一个jwtFilter,让所有请求进来前都走jwtFilter这个方法

5、创建JwtFilter并实现AuthenticatingFilter接口,后端接受请求时都会先走jwtFilter,从请求中获取请求头,如果请求头中带有Authorization : token 这样一个kv键值对,即收到的这个请求时带有token的,就封装成JwtToken并返回,也就是new JwtToken(token),注意:传入的参数token是我们从前端发出请求的请求头中获取的,获取方式是 (HttpservletRequest)servletRequest.getHeader("Authorizaton")

6、上一步说到我们如果从前端请求拿到的token不为空,该方法就返回一个JwtToken类,所以我们需要创建一个JwtToken的工具类,专门用来封装传入的token

7、在jwtFilter的createToken方法返回封装好的jwtToken,会在我们自定义jwtFilter继承的AuthenticatingFilter类中的executeLogin方法里面去调用我们jwtFilter的createdToken方法,并拿到我们返回的jwtToken,在subject.login(token),这个token是被转换过类型的AuthenticationToken token = this.createToken(request, response);

8、上一步说到在AuthenticatingFilter类中的executeLogin方法里进行认证subject.login(token),最终会走Realm里面进行认证,在shiroConifg里面的**securityManger()**方法里面也需要传入Realm,所以我们要自定义个Realm,并继承AuthorizingRealm

9、我们自定义一个Realm去继承AuthorizingRealm,并重写supports方法,让其返回的token是jwtToken

10、我们回到JwtFilter,在进行登录后对请求进行的一个拦截,走onAccessDenied()方法判断jwt是否过期失效,如果token为空的时候,我们不需要交给shiro进行登录处理,直接返回true,不需要在进行拦截,让其去访问没有添加认证注解的公共类。如果jwt不为空,就走shiro登录处理,使用jwtUtils判断jwt时效性

11、我们导入了jjwt依赖,但是还不够满足我们的需求,所以需要自定义一个jwtUtils,通过application.yml对该工具类的一些属性进行赋值,该工具类可以生成token,getClaimByToken(token)进行一个校验,isTokenExpired判断是否过期。

12、回到jwtFilter的onAccessDenied()继续判断token是否有效,判断正常就会进行一个登录处理,执行executeLogin()方法, 在AuthenticationFilter里的executeLogin(),会交给subject.login(token),最终走的是自定义的Realm的doGetAuthentiactionInfo进行认证处理

13、上一步我们说到最终会走到自定义Realm的doGetAuthenticationInfo进行认证,而认证不通过也就是通过token拿到的用户信息不存在或者已被锁定(某些操作权限被禁止),就会抛出异常,这个异常会在AuthenticatingFilter的executeLogin()方法进行一个捕获,返回一个onLoginFailure()的异常方法,而我们做的是前后端分离,并且我们返回前端的数据是有一定的格式,也就是封装成Result类,所以当出现这样一个异常的时候,我们最好就对这个onLoginFailure()方法进行一个重写,让其返回我们想要看到的结果

14、接下来是shiro的登录逻辑,前面我们讲到后端接受到请求会被ShiroFilter里面shiroFilterFactoryBean()方法定义的jwtFilter所拦截,如果是初次登陆,就会根据用户名密码走到jwtFilter里面createdToekn()放在,最终走到AuthenticatingFiler里面的createdToken,实际上也是调用UsernamePasswordToken,这跟我们之前使用原生的jwt生成方法一样。如果不是初次登陆,就会判断token的有效性最后走到我们自定义的Realm进行一个认证授权处理

15、我们shiro的登录逻辑开发实际上就是对Realm进行一个代码编写,因为shiro三大核心就是subject,SecurityMessage,还有Realm,所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager,所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject,SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法

16、自定义Realm,为了解析我们的jwtToken,我们需要注入JwtUtils,使用jwtUtils.getClaimByToken()去获取jwtToken的身份信息,jwtToken.getPrincipal().getSubject()方法是可以直接拿到用户id,在通过id查询出用户信息,在对拿到的用户信息进行非空判断,在第13话我们提到会在这个方法抛出异常。通过非空判断后需要返回SimpleAuthenticationInfo(),其中是三个参数分别为用户的基本信息,密钥信息,用户名字,而用户的基本信息是不包括用户的敏感信息的,但是我们通过id查询到的用户信息是完整的,这里我们定义了一个AccountProfile这样的一个VO实体类,来保存用户的基本信息,同样这个类也需要实现序列化。我们使用springframework.BeanUtil.copyProperties进行实体类复制,AccountProfile只会拿到自己拥有的属性

考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,我们找到了一个starter可以快速整合shiro-redis,配置简单,这里也推荐大家使用。

而因为我们需要做的是前后端分离项目的骨架,所以一般我们会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,我们需要引入jwt的身份验证过程。

1、实现流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YuEBaSEF-1619401627871)(VueBlog.assets/image-20210419184126146.png)]

2、定义配置类

1、Shiro-redis的引入

导入依赖

<!--shiro-redis-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>
        <!--hutool工具类 处理业务,简化代码-->
        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.3</version>
        </dependency>
        <!--jwt-->
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2、重写官方指定的方法

根据官方文档shiro-redis介绍

https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter

我们需要导入两个实现方法

The next step depends on whether you’ve created your own SessionManager or SessionsSecurityManager.

@Bean
public SessionManager sessionManager() {
    
    
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

    // inject redisSessionDAO
    sessionManager.setSessionDAO(redisSessionDAO);
    
    // other stuff...
    
    return sessionManager;
}

@Bean
public SessionsSecurityManager securityManager(List<Realm> realms, SessionManager sessionManager) {
    
    
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realms);

    //inject sessionManager
    securityManager.setSessionManager(sessionManager);

    // inject redisCacheManager
    securityManager.setCacheManager(redisCacheManager);
    
    // other stuff...
    
    return securityManager;
}

Then inject redisSessionDAO and redisCacheManager which created by shiro-redis-spring-boot-starter already

如果我们已经导入了shiro-redis-spring-boot-starter,就需要添加以下两个注解

@Autowired
RedisSessionDAO redisSessionDAO;

@Autowired
RedisCacheManager redisCacheManager;

注意:**public SessionsSecurityManager securityManager(List realms, SessionManager sessionManager)**方法中的Realm我们需要自己定义,所以必须重写这个方法

 @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
    
    
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        // inject redisSessionDAO
        sessionManager.setSessionDAO(redisSessionDAO);

        // other stuff...

        return sessionManager;
    }

    @Bean
    public SessionsSecurityManager securityManager(AccountRealm accountRealm,
                                                   SessionManager sessionManager,
                                                   RedisCacheManager redisCacheManager) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

        //inject sessionManager
        securityManager.setSessionManager(sessionManager);

        // inject redisCacheManager
        securityManager.setCacheManager(redisCacheManager);

        // other stuff...

        return securityManager;
    }

3、ShiroConif中添加ShiroFilterChainDefinition和ShiroFilterFactory

在流程图中有一个JwtFilter过滤器,判断用户有无jwt,是否放行访问

我们在这里使用这个过滤器

让所有的请求都去走jwt

@Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    
    
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

 @Bean("shiroFilterFactoryBean")
 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
       ShiroFilterChainDefinition shiroFilterChainDefinition) {
    
    
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

4、ShiroConfig中需要Realm

package com.shuang.shiro;

import com.shuang.entity.User;
import com.shuang.service.UserService;
import com.shuang.utils.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AccountRealm extends AuthorizingRealm {
    
    

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserService userService;

    // 支持的是jwtToken而不是token
    @Override
    public boolean supports(AuthenticationToken token) {
    
    
        return token instanceof JwtToken;
    }

    // 授权
    // 拿到用户获取权限信息然后封装成Authorization返回给我们的Shiro
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
    
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
       return null;
    }

}

5、创建JwtFilter

JwtFilter继承了AuthenticatingFilter 实现了基本登录的逻辑

在通过拦截jwt,并进行校验,我们需要创建JwtUtils工具类

下面截图中login最终走的是Realm类,进行token的认证和授权

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7VypcLoF-1619401649104)(VueBlog.assets/image-20210425085125732.png)]

package com.shuang.shiro;

import cn.hutool.json.JSONUtil;
import com.shuang.common.lang.Result;
import com.shuang.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

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


public class JwtFilter extends AuthenticatingFilter {
    
    

    @Autowired
    JwtUtils jwtUtils;

    // 用户登录完成后会返回一个jwt给用户
    // createToken 主要是调用login方法,并且拿到token,在通过shiro进行认证处理,主要在自定义的Realm中进行身份认证和授权,doGetAuthorizationInfo,doGetAuthenticationInfo
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
    
    

        HttpServletRequest request = (HttpServletRequest)servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)){
    
    
            return null;
        }

        return new JwtToken(jwt);
    }

    // 拦截  获取到jwt之后是否过期或者凭证不对的细节校验
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
    
    

        // 
        
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        String jwt = request.getHeader("Authorization");
        // 如果token是空的,让其直接去访问xxxController,但是有通过@RequireRole这么一个注解进行权限过滤,如果有些xxxController没有这样一个注解,则说明是一个公共页面,比如注册页面,登录页面
        if (StringUtils.isEmpty(jwt)){
    
    
            return true;
        }else {
    
    
            // 效验jwt
            Claims claim = jwtUtils.getClaimByToken(jwt);
            // 校验jwt是否为空,或者过期
            if (claim==null||jwtUtils.isTokenExpired(claim.getExpiration())) {
    
    
                throw new ExpiredCredentialsException("token已失效,请重新登录");
            }
            // jwt不为空且状态正常我们将进行登录处理
            // 这个executeLogin会拿到我们的token信息,去交给我们自定义的Realm进行登录Subject.login(token),最终会走到我们自定义Realm的goGetAuthenticationInfo(),在executeLogin中对token进行subject.login(token),就会走OnLoginFailure这样一个异常方法
            return executeLogin(servletRequest,servletResponse);
        }
    }

    // 在走到这个方法会抛出异常,而我们是一个前后端分离的项目,而且我们抛出的这个数据是有一定格式的,是Result的一个格式,所以当jwt的内容或者状态出现异常的时候,我们要对onLoginFailure进行重写,封装成result的格式抛出这个异常
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
    
    

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();

        Result result = Result.fail(throwable.getMessage());

        String json = JSONUtil.toJsonStr(result);

        try {
    
    
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {
    
    

        }

        return false;
    }
}
@Bean
    JwtFilter jwtFilter() {
    
    
        return new JwtFilter();
    }

需要将Jwt的信息保存到Token里面

6、自定义JwtToken

package com.shuang.shiro;

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;
    }
}

7、自定义JwtUtils工具类

定义好了jwtUtils工具类之后我们会在JwtFilter中对jwt进行一个校验处理

package com.shuang.utils;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * jwt工具类
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "shuang.jwt")
public class JwtUtils {
    
    

    // 密钥
    private String secret;
    // 超时时间
    private long expire;
    // 信息
    private String header;

    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
    
    
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

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

    public Claims getClaimByToken(String token) {
    
    
        try {
    
    
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
    
    
            log.debug("validate is token error ", e);
            return null;
        }
    }

    /**
     * token是否过期
     * @return  true:过期
     */
    public boolean isTokenExpired(Date expiration) {
    
    
        return expiration.before(new Date());
    }
}

8、添加jwt的配置文件

shuang:
  jwt:
    # 加密钥匙
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token 有效时长,7天 单位秒
    expire: 604800
    header: Authorization

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFsSS8xS-1619401649106)(VueBlog.assets/image-20210420132422331.png)]

9、shiro-redis补充

application.yml添加shiro-redis的配置

shiro-redis:
  enable: true
  reids-manager:
    host: 192.168.3.64:6379

猜你喜欢

转载自blog.csdn.net/weixin_46195957/article/details/116143810