关于使用Shiro+SpringBoot+redis实战

1.什么是shiro

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

2.shiro的核心架构

2.1 SecurityManager

SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。

SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。

2.2 Authenticator

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。

2.3 Authorizer

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。

2.4 Realm

Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

  • ​ 注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。

2.5 SessionManager

sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。

2.6 SessionDAO

SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。

2.7 CacheManager

CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。

2.8 Cryptography

Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

3.SpringBoot+Shiro+Redis整合

3.1引入依赖

        <!--JWT依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!--shiro-redis依赖-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.2.1</version>
        </dependency>

3.2 创建配置类

第一步:创建shiro过滤器,让shiro管理项目资源

    /**
     * 第一步:创建Shiro过滤器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition){
        //创建shiro过滤器
        //要让shiro管理web应用,首先需要让shiro接管资源,所以创建一个过滤器
        ShiroFilterFactoryBean shiroFilterFactoryBean= new ShiroFilterFactoryBean();
        //给拦截器设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        
        //这是自定义拦截器的一些函数方法
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", new JwtFilter());//设置自定义的拦截器
        shiroFilterFactoryBean.setFilters(filters);


        /**
         *   Map map = new LinkedHashMap<>();
         *   map.put("/**" , "authc");
         *   shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
         *   下面的方法作用和上面的一样,只不过专门使用一个shiroFilterChainDefinition()封装起来
         *   shiroFilterChainDefinition()里面的设置拦截器的拦截路径
         **/
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        return shiroFilterFactoryBean;
    }

第二步:创建上面使用的安全管理器

   /**
     * 第二步:创建安全管理器
     * 设置session和cache
     * @param accountRealm
     * @param sessionManager
     * @param redisCacheManager
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                     SessionManager sessionManager,
                                                     RedisCacheManager redisCacheManager) {
        //创建安全管理器
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
        //设置自定义Realm
        securityManager.setRealm(accountRealm);

        //设置自定义session管理
        securityManager.setSessionManager(sessionManager);

        // 设置redis的cache
        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

第三步:创建第一步使用到shiroFilterChainDefinition,设置哪些可以不用认证就可以访问,哪些资源受限

    /**
     * 配置公共资源和受保护资源
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {

        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        //退出登录
        filterMap.put("/logout", "logout");
        // swagger
        filterMap.put("/swagger**/**", "anon");
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/v2/**", "anon");

        filterMap.put("/**", "jwt"); // 使用自定义的拦截器进行拦截处理
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

第四步:设置第二步使用到的sessionManager

    /**
     * 自定义session,设置redisSessionDAO
     * @param redisSessionDAO
     * @return
     */
    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 注入redisSessionDAO,缓存为Redis则需改用Redis的管理
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }

3.3 创建自定义过滤器


public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    /**
     *如果原来的请求头有token,就返回JwtToken中,在executeLogin()方法使用createToken
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 获取 token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //获取头部信息
        String jwt = request.getHeader("token");
        if(StringUtils.isEmpty(jwt)){
            return null;
        }

        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 获取 token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //获取头部信息
        String token = request.getHeader("token");

        if(StrUtil.isEmpty(token)){
            //不进行拦截

            return true;
        }else {
            // 效验token,判断是否已过期
            System.out.println(token);
            //通过token拿到claim,效验claim
            Claims claim = jwtUtils.getClaimByToken(token);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                throw new ExpiredCredentialsException("token已失效,请重新登录!");
            }
        }
        // 执行自动登录,最后委托自定义Realm执行认证
        return executeLogin(servletRequest, servletResponse);
    }

    //登录失败,返回错误信息
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            //返回错误的信息
            JsonResult jsonResult = new JsonResult(ResponseStatusEnum.ERROR.getCode(),throwable.getMessage());
            //转换为json
            String json = JSONUtil.toJsonStr(jsonResult);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }

    //解决跨域的问题
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(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请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }

        return super.preHandle(request, response);
    }

}

3.4 自定义Realm

@Component
public class AccountRealm extends AuthorizingRealm {

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    SysAdminService sysAdminService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }


    /**
     * 认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //强制类型转换
        JwtToken jwtToken = (JwtToken) token;

        //没有token,认证失败
        if (jwtToken.getPrincipal() == null) {
            return null;
        }

        //获取账号
        String adminAccount = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();

        QueryWrapper<SysAdmin> queryWrapper =  new QueryWrapper<>();
        queryWrapper.eq("admin_account",adminAccount);
        SysAdmin admin = sysAdminService.getOne(queryWrapper);

        if (admin==null){
            throw new UnknownAccountException("账户不存在");
        }

        if (admin.getAdminEnabled()==0){
            throw new UnknownAccountException("账户已被禁用");
        }
        //设置返回的信息
        AccountProfile profile = new AccountProfile();
        BeanUtil.copyProperties(admin,profile);

        return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName());

    }
}

3.5 一些使用到的工具类

  1. JWT工具类
    JwtUtils.java
/**
 1. jwt工具类
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "manage.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();
    }

    /**
     * 获取token中的负载信息
     * @param token
     * @return
     */
    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());
    }
}
  1. AuthenticationToken的实现类
/**
 * AuthenticationToken的实现类
 * 具体实现过程在这里面
 */
public class JwtToken implements AuthenticationToken {

    private String token;

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

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

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

3.6整个项目整合思路

首先,所有请求交给ShiroFilter的过滤器,由过滤器进行管理。shiroFilterFactoryBean设定安全管理器securityManager和自定义过滤器JwtFilter,安全管理器securityManager设定自定义的Realm。

再者,自定义一个过滤器对请求进行拦截,对于没有token进行放行,对于有token的,效验token是否正确和是否失效。如果都正确就执行executeLogin(),调用Realm的**doGetAuthenticationInfo()**对其进行认证。
关于使用Shiro+SpringBoot+redis实战_第1张图片

你可能感兴趣的:(shiro,redis,java,数据库)