• java入门笔记五(springboot框架博客系统开发2)


    继续博客网站后端的开发。

    引入shiro和jwt. shiro用来缓存和会话信息,存储在redis里面,jwt作为跨域身份验证解决方案。

    首先是常规jwt的逻辑图:

    引入shiro之后的逻辑图

    1 导入shiro和jwt,主要用于缓存和会话信息。首先需要装一下redis,本机mac安装步骤如下,安装之后配置自启动

    step1:去官网下载redis稳定版
    step2:解压,移动到你的mac的/user/local目录下
    step3:开始执行命令:make  然后 make install
    step4:进入redis的安装目录,启动redis服务:redis-server
    step5 如果你想要你的redis在后台启动,修改redis.conf中的daemonize yes,然后重启服务:redis-server ./redis.conf

    2 pom.xml导入shiro-redis的starter包:还有jwt的工具包,以及hutool工具包(utils类包)。

    <dependency>
        <groupId>org.crazycake</groupId>
        <artifactId>shiro-redis-spring-boot-starter</artifactId>
        <version>3.2.1</version>
    </dependency>
    <!-- hutool工具类-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.3.3</version>
    </dependency>
    <!-- jwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    

    3 重写shiro配置文件,首先新建一个com.blog.config.ShiroConfig类,意义如下

    • 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
    • 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
    • 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。

    代码如下:

    package com.blog.config;
    
    import com.blog.shiro.AccountRealm;
    import com.blog.shiro.JwtFilter;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.session.mgt.SessionManager;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
    import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.crazycake.shiro.RedisCacheManager;
    import org.crazycake.shiro.RedisSessionDAO;
    import org.springframework.beans.factory.annotation.Autowired;
    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;
    
    /**
     * shiro启用注解拦截控制器
     */
    @Configuration
    public class ShiroConfig {
    
        @Autowired
        JwtFilter jwtFilter;
        /**
         * session域管理
         * @param
         * @return
         */
        @Bean
        public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
            // inject redisSessionDAO 注入
            sessionManager.setSessionDAO(redisSessionDAO);
            // other stuff...
            return sessionManager;
        }
    
        /**
         * 重写shiro的安全管理容器,
         * @param
         * @param sessionManager
         * @param redisCacheManager
         * @return
         */
        @Bean
        public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                       SessionManager sessionManager,
                                                       RedisCacheManager redisCacheManager) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
            //inject sessionManager
            securityManager.setSessionManager(sessionManager);
            // inject redisCacheManager
            securityManager.setCacheManager(redisCacheManager);
            // other stuff...
            //关闭shiro自带的session,详情见文档
    //        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
    //        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
    //        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
    //        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
    //        securityManager.setSubjectDAO(subjectDAO);
            return securityManager;
    
        }
    
        /**
         * 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,
         * 而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,
         * 有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,
         * 比如@RequiresAuthentication,这样控制权限访问。
         * @return
         */
        @Bean
        public ShiroFilterChainDefinition shiroFilterChainDefinition() {
            //申请一个默认的过滤器链
            DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
            Map<String, String> filterMap = new LinkedHashMap<>();
            //添加一个jwt过滤器到过滤器链中
            filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
            chainDefinition.addPathDefinitions(filterMap);
            return chainDefinition;
        }
    
        /**
         * 过滤器工厂业务
         * @param securityManager
         * @param shiroFilterChainDefinition
         * @return
         */
        @Bean("shiroFilterFactoryBean")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                             ShiroFilterChainDefinition shiroFilterChainDefinition) {
            /*shiro过滤器bean对象*/
            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 写shiro包的类,首先新建com.blog.shiro.AccountRealm类,继承shiro的AuthorizingRealm类并重写三个方法,

    • supports:为了让realm支持jwt的凭证校验
    • doGetAuthorizationInfo:权限校验
    • doGetAuthenticationInfo:登录认证校验

    代码如下

    package com.blog.shiro;
    
    import cn.hutool.core.bean.BeanUtil;
    import com.blog.entity.User;
    import com.blog.service.UserService;
    import com.blog.utils.JwtUtils;
    import lombok.extern.slf4j.Slf4j;
    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.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    @Slf4j        //可以使用log打印日志
    @Component
    public class AccountRealm extends AuthorizingRealm {
    
        @Autowired
        JwtUtils jwtUtils;
    
        @Autowired
        UserService userService;
    
        /**
         * 判断是否为jwt的token
         * @param token
         * @return
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
    
        /**
         * 权限验证
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            return null;
        }
    
        /**
         * 登录认证
         * @param token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            JwtToken jwt = (JwtToken) token;
            // 将传入的AuthenticationToken强转JwtTokenJ
            log.info("jwt----------------->{}", jwt);
            // 获取jwtToken中的userId
            String userId = jwtUtils.getClaimByToken((String)jwt.getPrincipal()).getSubject();
            // 根据jwtToken中的userId查询数据库
            User user = userService.getById(Long.parseLong(userId));
            if(user == null) {
                throw new UnknownAccountException("账户不存在!");
            }
            if(user.getStatus() == -1) {
                throw new LockedAccountException("账户已被锁定!");
            }
            // 将可以显示的信息放在该载体中,对于密码这种隐秘信息不需要放在该载体中
            AccountProfile profile = new AccountProfile();
            BeanUtil.copyProperties(user, profile);
            log.info("profile----------------->{}", profile.toString());
    
            //封装成SimpleAuthenticationInfo返回给shiro
            return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
        }
    
    }

    5 创建JwtToken类,新建com.blog.shiro.JwtToken类,用来实现AuthenticationToken接口,因为shiro默认supports

    支持的是UsernamePasswordToken,而我们采用jwt的方式,故需要定义一个JwtToken来重写该token。

    package com.blog.shiro;
    
    import org.apache.shiro.authc.AuthenticationToken;
    
    /**
     * shiro默认supports支持的是UsernamePasswordToken,
     * 而我们采用jwt的方式,故需要定义一个JwtToken来重写该token。
     */
    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;
        }
    
    }

    6 接着创建工具类com.blog.utils.JwtUtils类,用于生成和校验jwt,其中jwt相关的密钥信息是从项目的配置文件中获取的。

    package com.blog.utils;
    
    import io.jsonwebtoken.Claims;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Slf4j   //可以使用log打印日志
    @Data
    @Component
    @ConfigurationProperties(prefix="blog.jwt") //配合application.properties 中加入的配置
    public class JwtUtils {
        private String secret;
        private long expire;
        private String header;
        /**
         * 生成jwt token
         */
        public String generateToken(long userId) {
            return null;
        }
    
        // 获取jwt的信息
        public Claims getClaimByToken(String token) {
            return null;
        }
    
        /**
         * token是否过期
         * @return  true:过期
         */
        public boolean isTokenExpired(Date expiration) {
            return expiration.before(new Date());
        }
    }

    在之前新建的src/main/resources/application.yml追加配置如下:

    shiro-redis:
      enabled: true
      redis-manager:
        host: 127.0.0.1:6379
    blog:
      jwt:
        # 加密秘钥
        secret: f4e2e52034348f86b67cde581c0f9abc
        # token有效时长,7天,单位秒
        expire: 604800
        header: token

    7 加上登录成功返回的一个用户信息类AccountProfile,新增com.blog.shiro.AccountProfile类,

    package com.blog.shiro;
    
    import lombok.Data;
    import java.io.Serializable;
    @Data
    public class AccountProfile implements Serializable {
        private Long id;
        private String username;
        private String avatar;
    }

    8 定义jwt的过滤器JwtFilter,继承Shiro内置的AuthenticatingFilter,内置了可以自动登录方法的的过滤器。重写4个方法

         createToken:实现登录,我们需要生成我们自定义支持的JwtToken

         onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;

                          当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录

         onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出

         preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,

                          我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。

    package com.blog.shiro;
    
    import cn.hutool.json.JSONUtil;
    import com.blog.common.lang.Result;
    import com.blog.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.apache.shiro.web.filter.authc.AuthenticationFilter;
    import org.apache.shiro.web.util.WebUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    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.IOException;
    
    @Component
    public class JwtFilter extends AuthenticatingFilter {
        @Autowired
        JwtUtils jwtUtils;
    
        /**
         * 实现登录,生成自定义的JwtToken
         * @param servletRequest
         * @param servletResponse
         * @return
         * @throws Exception
         */
        @Override
        protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            // 获取 token
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String jwt = request.getHeader("Authorization");
            if(StringUtils.isEmpty(jwt)){
                return null;
            }
            return new JwtToken(jwt);
        }
    
        /**
         * 拦截校验
         * @param servletRequest
         * @param servletResponse
         * @return
         * @throws Exception
         */
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader("Authorization");
            //没有token,直接通过
            if(StringUtils.isEmpty(token)) {
                return true;
            } else {
                // 判断是否已过期
                Claims claim = jwtUtils.getClaimByToken(token);
                if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                    throw new ExpiredCredentialsException("token已失效,请重新登录!");
                }
            }
            // 执行自动登录
            return executeLogin(servletRequest, servletResponse);
        }
    
        /**
         * 登录失败
         * @param token
         * @param e
         * @param request
         * @param response
         * @return
         */
        @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();
                //获取登陆异常信息以自定义的Resut响应格式返回json数据
                Result r = Result.fail(throwable.getMessage());
                String json = JSONUtil.toJsonStr(r);
                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);
        }
    }

    shiro整合完毕,并且使用了jwt进行身份校验。

  • 相关阅读:
    批处理
    命名规则
    注释
    HTML DOM属性
    OLTP
    修改HTML元素
    HTML
    工具资源系列之给虚拟机装个centos
    工具资源系列之给虚拟机装个windows
    工具资源系列之给mac装个虚拟机
  • 原文地址:https://www.cnblogs.com/mengsx/p/14406240.html
Copyright © 2020-2023  润新知