美文网首页
shiro整合jwt

shiro整合jwt

作者: 拼搏男孩 | 来源:发表于2020-05-27 22:28 被阅读0次

    shiro整合jwt

    这篇文章参考了这个网址:https://github.com/HowieYuan/Shiro-SpringBoot

    一、添加依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.6.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.qianfeng</groupId>
        <artifactId>shiro-jwt</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>shiro-jwt</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.16</version>
            </dependency>
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.0</version>
            </dependency>
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.2.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    这是一个springboot项目,除了常规的springboot的依赖和数据库的依赖,最重要的是shiro和jwt的依赖,分别是shiro-spring与java-jwt。

    二、项目结构

    [图片上传失败...(image-6f4263-1590589772559)]

    如上图所示,config包是配置包,ShiroConfig是Shiro的配置类,用来对shiro进行一些自定义配置。filter是过滤器,因为我们使用了jwt所以需要自定义过滤器。然后是model,由于这是一个前后端分离的项目,在向前端返回数据的时候返回的是json格式的数据,同时还要封装一些状态信息等,所以自定义这个类用来完成对状态、对象的封装。shiro包下面的两个类一个JWTToken是自定义的Token,MyRealm是自定义的Realm。util包下的JWTUtil用来生成并校验token。

    三、JWTUtil

    package com.qianfeng.util;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTDecodeException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.io.UnsupportedEncodingException;
    import java.util.Date;
    
    /**
     * @author huwen
     */
    public class JWTUtil {
        /**
         * 设置过期时间24小时
         */
        private static final long EXPIRE_TIME = 1000*60*60*24;
        /**
         * 设置密钥
         */
        private static final String SECRET = "shiro+jwt";
    
        /**
         * 根据用户名创建一个token
         * @param username 用户名
         * @return 返回的token字符串
         */
        public static String createToken(String username){
            try {
                //将当前时间的毫秒数和设置的过期时间相加生成一个新的时间
                Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
                //由密钥创建一个指定的算法
                Algorithm algorithm = Algorithm.HMAC256(SECRET);
                return JWT.create()
                        //附带username信息
                        .withClaim("username",username)
                        //附带过期时间
                        .withExpiresAt(date)
                        //使用指定的算法进行标记
                        .sign(algorithm);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 验证token是否正确
         * @param token 前端传过来的token
         * @param username 用户名
         * @return 返回boolean
         */
        public static boolean verify(String token,String username){
            try {
                //获取算法
                Algorithm algorithm = Algorithm.HMAC256(SECRET);
                //生成JWTVerifier
                JWTVerifier verifier = JWT.require(algorithm)
                        .withClaim("username",username)
                        .build();
                //验证token
                verifier.verify(token);
                return true;
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 从token中获得username,无需secret
         * @param token token
         * @return username
         */
        public static String getUsername(String token){
            try {
                DecodedJWT jwt = JWT.decode(token);
                return jwt.getClaim("username").asString();
            } catch (JWTDecodeException e) {
                return null;
            }
        }
    }
    

    这个类用来完成一些对token的操作:创建token、验证token、从token中获得username。创建token的时候需要指定token的过期时间,以及secret,同样,验证的时候也需要secret。最后,从token中获取username并不需要secret。

    四、JWTFilter

    package com.qianfeng.filter;
    
    import com.qianfeng.shiro.JWTToken;
    import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    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.IOException;
    import java.net.URLEncoder;
    
    
    public class JWTFilter extends BasicHttpAuthenticationFilter {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            //判断请求头是否带上“Token”
            if(isLoginAttempt(request, response)){
                //如果存在,则执行executeLogin方法登入,检查token是否正确
                try {
                    executeLogin(request, response);
                    return true;
                } catch (Exception e) {
                    responseError(response,e.getMessage());
                }
            }
            //如果没有token,则可能是执行登录操作或者是游客状态访问,无需检查token,直接返回true
            return true;
        }
    
        /**
         * 将非法请求跳转到 /unauthorized/**
         * @param response
         * @param message
         */
        private void responseError(ServletResponse response, String message) {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            try {
                message = URLEncoder.encode(message,"UTF-8");
                httpServletResponse.sendRedirect("/unauthorized/"+message);
            } catch (IOException e) {
                logger.error(e.getMessage());
            }
        }
    
        /**
         * 判断用户是否想要登入
         * 检测header里面是否包含Token字段
         * @param request request
         * @param response response
         * @return boolean
         */
        @Override
        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
            HttpServletRequest req = (HttpServletRequest) request;
            String token = req.getHeader("Token");
            return token != null;
        }
    
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = httpServletRequest.getHeader("Token");
            JWTToken jwtToken = new JWTToken(token);
            //提交给realm进行登入,如果错误就会抛出异常并被捕获
            getSubject(request, response).login(jwtToken);
            return true;
        }
    
        /**
         * 对跨域访问提供支持
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @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);
        }
    }
    

    这个类继承了BasicHttpAuthenticationFilter并重写了里面的部分方法。最重要的是executeLogin这个方法,这个方法从请求头中获取token这个字段然后构造出一个JWTToken对象进行登录,实际上就是提交给MyRealm,和我们之前的UsernamePasswordToken登录的方式一样。

    五、JWTToken

    package com.qianfeng.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;
        }
    }
    

    这个类实现了AuthenticationToken认证Token这个接口,UsernamePasswordToken也是实现了这个接口,都可以用作认证。

    六、MyRealm

    package com.qianfeng.shiro;
    
    import com.qianfeng.pojo.Employee;
    import com.qianfeng.pojo.Permission;
    import com.qianfeng.pojo.Roles;
    import com.qianfeng.service.EmployeeService;
    import com.qianfeng.util.JWTUtil;
    import org.apache.shiro.authc.*;
    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;
    
    @Component("myRealm")
    public class MyRealm extends AuthorizingRealm {
        @Resource
        private EmployeeService employeeService;
    
        /**
         * 必须重写此方法,否则会报错
         * @param token
         * @return
         */
        @Override
        public boolean supports(AuthenticationToken token){
            return token instanceof JWTToken;
        }
        /**
         * 授权方法
         * @param principalCollection principal的集合,可以理解为各种用户身份的集合,比如用户名、邮箱、手机号等
         * @return 返回的是授权信息,包括角色与权限
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            String username = JWTUtil.getUsername(principalCollection.toString());
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            List<Roles> roles = employeeService.getAllRolesByEmpName(username);
            Set<String> roleSet = new HashSet<>();
            Set<String> permissionSet = new HashSet<>();
            for (Roles role : roles) {
                roleSet.add(role.getRoleName());
            }
            List<Permission> permissions = employeeService.getAllPermissionsByEmpName(username);
            for (Permission permission : permissions) {
                permissionSet.add(permission.getPermName());
            }
            info.setRoles(roleSet);
            info.setStringPermissions(permissionSet);
            return info;
        }
    
        /**
         * 这个方法用于认证
         * @param authenticationToken 用户名与密码
         * @return 认证信息
         * @throws AuthenticationException 可能引发的异常
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //获得token
            String token = (String) authenticationToken.getCredentials();
            //从token中获得username
            String username = JWTUtil.getUsername(token);
            //如果username为空或者验证不匹配
            if(username == null||!JWTUtil.verify(token,username)){
                throw new AuthenticationException("token认证失败!");
            }
            String password = employeeService.getPassword(username);
            //如果没有查询到用户名对应的密码
            if(password==null){
                throw new AuthenticationException("该用户不存在");
            }
            return new SimpleAuthenticationInfo(token,token,"MyRealm");
        }
    }
    

    MyRealm继承了AuthorizingRealm,授权Realm,重写其中的认证和授权方法。

    七、ShiroConfig

    package com.qianfeng.config;
    
    import com.qianfeng.filter.JWTFilter;
    import com.qianfeng.shiro.MyRealm;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    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;
    
    @Configuration
    public class ShiroConfig {
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            //创建自定义过滤器
            Map<String, Filter> filterMap = new LinkedHashMap<>();
            //将JWTFilter命名为jwt
            filterMap.put("jwt",new JWTFilter());
            shiroFilterFactoryBean.setFilters(filterMap);
            //设置securityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            //设置无权限时跳转的url
            shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized/无权限");
            shiroFilterFactoryBean.setLoginUrl("/login");
            Map<String,String> filterRuleMap = new HashMap<>(2);
            //所有请求通过我们自己的过滤器
            filterRuleMap.put("/**","jwt");
            //匿名用户可以访问的url
            filterRuleMap.put("/unauthorized/**","anon");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
            return shiroFilterFactoryBean;
        }
        @Bean
        public SecurityManager securityManager(MyRealm myRealm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(myRealm);
            //关闭shiro自带的session
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            return securityManager;
        }
        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            // 强制使用cglib,防止重复代理和可能引起代理出错的问题
            // https://zhuanlan.zhihu.com/p/29161098
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    }
    

    这个类是Shiro的配置类,设置好我们自定义的 filter,并使所有请求通过我们的过滤器,除了我们用于处理未认证请求的 /unauthorized/**

    八、权限控制注解

    主要通过shiro的@RequiresRoles和@RequiresPermissions注解进行权限控制,这两个注解放在controller的返回方法上,如果不具有相应的角色或权限就会抛出异常

    相关文章

      网友评论

          本文标题:shiro整合jwt

          本文链接:https://www.haomeiwen.com/subject/fthnahtx.html