Shiro

一、什么是shiro

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

shiro主要有三个核心组件组成: Subject 、SecurityMannager 和Realms

  • Subject :即"当前操作用户"

  • SecurityMannager: shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务

  • Realm: Realm充当了Shiro与应用安全数据间的"桥梁"或者"连接器",也就说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户以及其权限信息

所以,Realm相当于一个安全相关的DAO,它封装了数据源的连接细节,并在需要时将相关的数据提供给shiro,当配置Shiro时,你必须至少指定一个Realm,用于认证或授权,配置多个Realm是可以的,但是至少需要一个

常用的功能:

Authentication: 身份认证/登录(账号密码验证)

Authoriztion: 授权,即角色或者权限验证 (权限)

Session Manager: 会话管理,用户登录之后Session 相关管理

Cryptography: 用于密码的加密(一般是以md5进行加密)

Web Support: 集成于Web环境

Caching: 缓存,可以将用户信息、角色、权限等缓存到Redis中,这样就不用在每次去查询数据库

首先,在ShiroConfig配置的会ShiroFilterFactoryBean,会过滤掉不需要验证的路径,如登录请求,在第一次登录时,会将token放入redis缓存,而需要验证的路径通过自定义的JwtFilter取出请求Header中的token,调用getSubject(request, response).login(jwtToken)进一步验证token,在调用login后,进入自定义CustomRealm,调用父类AuthorizingRealmdoGetAuthenticationInfo(AuthenticationToken authenticationToken)方法,进行具体的token验证,验证成功后,将token中的信息取出构建SimpleAuthenticationInfo,并返回。

如果Controller配置了@RequiresPermissions("perms")注解,并且没有被过滤的请求,则会进入自定义CustomRealmdoGetAuthorizationInfo(PrincipalCollection principalCollection),在这里通过SimpleAuthenticationInfo中包含用户信息进而获取用户权限并,然后shiro会比对perms。但是需要注意每次合规请求都会走这里,所以需要可以在ShiroConfig中配置redis缓存用户权限,在doGetAuthorizationInfo之前会先读缓存,缓存中有就会直接跳过,但是需要注意修改权限后,需要刷新redis缓存。

二、springboot 整合shiro

  1. pom相关jar

     <!-- shiro -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.spring.version}</version>
    </dependency>
    ​
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>${shiro.spring.version}</version>
    </dependency>
    ​
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.spring.version}</version>
    </dependency>
    ​
    <!-- shiro-redis -->
    <dependency>
        <groupId>org.crazycake</groupId>
        <artifactId>shiro-redis</artifactId>
        <version>${shiro-redis.version}</version>
        <exclusions>
            <exclusion>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    ​
    <!-- 添加jwt的依赖 -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>${java.jwt.version}</version>
    </dependency>

  2. 自定义Realm

    public class CustomRealm extends AuthorizingRealm {
    ​
        @Autowired
        private SysUserService sysUserService;
    ​
        @Autowired
        private RedisUtil redisUtil;
    ​
        /**
         * 必须重写此方法,不然Shiro会报错
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
    ​
        /**
         * 根据当前登录人来返回对应授权配置类  权限的处理
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            // 获取登录的用户名
            JwtInfo jwtInfo = (JwtInfo) principalCollection.getPrimaryPrincipal();
            // 查询用户的名称
            SysUserVO sysUser = sysUserService.queryUserAuth(jwtInfo.getUsername());
            // 添加角色和权限
            SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
            for(SysRole role : sysUser.getRoles()) {
                // 添加角色
                simpleAuthenticationInfo.addRole(role.getRoleName());
                // 添加权限
                for(SysPermission permission : role.getPermissions()) {
                    simpleAuthenticationInfo.addStringPermission(permission.getPerms());
                }
            }
    ​
            return simpleAuthenticationInfo;
        }
    ​
        /**
         * 根据表单提交上的用户名来创建出 权限配置类  用于姓名和密码的校验
         * @param authenticationToken
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            Object tokenObject = authenticationToken.getCredentials();
            if (Objects.isNull(tokenObject)) {
                throw new AuthenticationException("token为空!");
            }
    ​
            String token = tokenObject.toString();
            // 校验token有效性
            JwtInfo jwtInfo = null;
            try {
                jwtInfo = this.checkUserTokenIsEffect(token);
            } catch (AuthenticationException e) {
                e.printStackTrace();
                return null;
            }
    ​
            return new SimpleAuthenticationInfo(jwtInfo, token, getName());
        }
    ​
        /**
         * 校验token的有效性
         *
         * @param token
         */
        public JwtInfo checkUserTokenIsEffect(String token) throws AuthenticationException {
            // 解密获得username,用于和数据库进行对比
            String username = JwtUtil.getUsername(token);
            if (username == null) {
                throw new AuthenticationException("token非法无效!");
            }
    ​
            // 查询用户信息
            LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysUser::getUsername, username);
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 判断用户状态
    ​
            // 校验token是否超时失效 & 或者账号密码是否错误
            JwtInfo jwtInfo = new JwtInfo();
            if (!jwtTokenRefresh(token, username, sysUser.getPassword())) {
                throw new AuthenticationException("Token失效,请重新登录!");
            } else {
                BeanUtils.copyProperties(sysUser, jwtInfo);
            }
    ​
            return jwtInfo;
        }
    ​
        /**
         * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
         * 先验证jwt中token是否有效,无效,再验证redis,中token作为key是否存在,如果无效则证明过期
         * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
         * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
         * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
         * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
         * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
         *       用户过期时间 = Jwt有效时间 * 2。
         *
         * @param username
         * @param password
         * @return
         */
        public boolean jwtTokenRefresh(String token, String username, String password) {
            String cacheToken = String.valueOf(redisUtil.get(RedisKeyConstant.PREFIX_USER_TOKEN + token));
            if (oConvertUtils.isNotEmpty(cacheToken)) {
                // 校验token有效性
                if (!JwtUtil.verify(cacheToken, username, password)) {
                    String newAuthorization = JwtUtil.sign(username, password);
                    // 设置超时时间
                    redisUtil.set(RedisKeyConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                    redisUtil.expire(RedisKeyConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
                }
                return true;
            }
            return false;
        }
    }
    public class JwtToken implements AuthenticationToken {
        
        private static final long serialVersionUID = 1L;
        private String token;
     
        public JwtToken(String token) {
            this.token = token;
        }
     
        @Override
        public Object getPrincipal() {
            return token;
        }
     
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    public class JwtInfo implements Serializable {
        private String username;
        private String password;
        private String id;
        private String realname;
    ​
        public JwtInfo() {
    ​
        }
    ​
        public JwtInfo(String username, String password, String id, String realname) {
            this.username = username;
            this.password = password;
            this.id = id;
            this.realname = realname;
        }
    ​
        public String getUsername() {
            return username;
        }
    ​
        public void setUsername(String username) {
            this.username = username;
        }
    ​
        public String getPassword() {
            return password;
        }
    ​
        public void setPassword(String password) {
            this.password = password;
        }
    ​
        public String getId() {
            return id;
        }
    ​
        public void setId(String id) {
            this.id = id;
        }
    ​
        public String getRealname() {
            return realname;
        }
    ​
        public void setRealname(String name) {
            this.realname = name;
        }
    ​
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
    ​
            JwtInfo jwtInfo = (JwtInfo) o;
    ​
            if (username != null ? !username.equals(jwtInfo.username) : jwtInfo.username != null) {
                return false;
            }
            return id != null ? id.equals(jwtInfo.id) : jwtInfo.id == null;
    ​
        }
    ​
        @Override
        public int hashCode() {
            int result = username != null ? username.hashCode() : 0;
            result = 31 * result + (id != null ? id.hashCode() : 0);
            return result;
        }
    }

  3. 创建Shiro配置文件

    @Configuration
    public class ShiroConfig {
    ​
        @Resource
        LettuceConnectionFactory lettuceConnectionFactory;
    ​
        @Autowired
        private Environment env;
    ​
        @Bean
        public Realm realm(){
            CustomRealm customerRealm = new CustomRealm();
            //开启缓存管理
            return customerRealm;
        }
    ​
        @Bean("securityManager")
        public DefaultWebSecurityManager securityManager(@Qualifier("realm") Realm realm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(realm);
    ​
            /*
             * 关闭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);
            //自定义缓存实现,使用redis
            securityManager.setCacheManager(redisCacheManager());
            return securityManager;
        }
    ​
        /**
         *  该类的作用是,实现注解的方式来设置权限
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
            DefaultAdvisorAutoProxyCreator advisor = new DefaultAdvisorAutoProxyCreator();
            advisor.setProxyTargetClass(true);
            return advisor;
        }
    ​
    ​
        /**
         * 实现注解的方式来配置权限
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor
        authorizationAttributeSourceAdvisor(SecurityManager securityManager){
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new
                    AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    ​
        /**
         * Filter Chain定义说明
         *
         * 1、一个URL可以配置多个Filter,使用逗号分隔
         * 2、当设置多个过滤器时,全部验证通过,才视为通过
         * 3、部分过滤器可指定参数,如perms,roles
         */
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // 拦截器
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            // 配置不会被拦截的链接 顺序判断
            filterChainDefinitionMap.put("/sys/user/login", "anon"); //登录接口排除
            filterChainDefinitionMap.put("/", "anon");
            filterChainDefinitionMap.put("/doc.html", "anon");
            filterChainDefinitionMap.put("/**/*.js", "anon");
            filterChainDefinitionMap.put("/**/*.css", "anon");
            filterChainDefinitionMap.put("/**/*.html", "anon");
            filterChainDefinitionMap.put("/**/*.svg", "anon");
            filterChainDefinitionMap.put("/**/*.pdf", "anon");
            filterChainDefinitionMap.put("/**/*.jpg", "anon");
            filterChainDefinitionMap.put("/**/*.png", "anon");
            filterChainDefinitionMap.put("/**/*.ico", "anon");
    ​
            //remote接口排除
            filterChainDefinitionMap.put("/remote/es/**", "anon");
    ​
            filterChainDefinitionMap.put("/druid/**", "anon");
            filterChainDefinitionMap.put("/swagger-ui.html", "anon");
            filterChainDefinitionMap.put("/swagger**/**", "anon");
            filterChainDefinitionMap.put("/webjars/**", "anon");
            filterChainDefinitionMap.put("/v2/**", "anon");
    ​
            filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
    ​
    ​
            //性能监控  TODO 存在安全漏洞泄露TOEKN(durid连接池也有)
            filterChainDefinitionMap.put("/actuator/**", "anon");
    ​
            // 添加自己的过滤器并且取名为jwt
            Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
    //        如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
            filterMap.put("jwt", new JwtFilter(true));
            shiroFilterFactoryBean.setFilters(filterMap);
    //         <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
            filterChainDefinitionMap.put("/**", "jwt");
    ​
            // 未授权界面返回JSON
    //        shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
    //        shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    ​
        /**
         * cacheManager 缓存 redis实现
         * 使用的是shiro-redis开源插件
         *
         * @return
         */
        public RedisCacheManager redisCacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager());
            //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
            redisCacheManager.setPrincipalIdFieldName("id");
            //用户权限信息缓存时间
            redisCacheManager.setExpire(200000);
            return redisCacheManager;
        }
    ​
        /**
         * 配置shiro redisManager
         * 使用的是shiro-redis开源插件
         *
         * @return
         */
        @Bean
        public IRedisManager redisManager() {
            IRedisManager manager;
            // redis 单机支持,在集群为空,或者集群无机器时候使用
            if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
                RedisManager redisManager = new RedisManager();
                redisManager.setHost(lettuceConnectionFactory.getHostName());
                redisManager.setPort(lettuceConnectionFactory.getPort());
                redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
                redisManager.setTimeout(0);
                if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
                    redisManager.setPassword(lettuceConnectionFactory.getPassword());
                }
                manager = redisManager;
            }else{
                // redis集群支持,优先使用集群配置
                RedisClusterManager redisManager = new RedisClusterManager();
                Set<HostAndPort> portSet = new HashSet<>();
                lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
                if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
                    JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
                            lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
                    redisManager.setPassword(lettuceConnectionFactory.getPassword());
                    redisManager.setJedisCluster(jedisCluster);
                } else {
                    JedisCluster jedisCluster = new JedisCluster(portSet);
                    redisManager.setJedisCluster(jedisCluster);
                }
                manager = redisManager;
            }
            return manager;
        }
    }
    @Slf4j
    public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
        @Override
        public Class getObjectType() {
            return MySpringShiroFilter.class;
        }
    ​
        @Override
        protected AbstractShiroFilter createInstance() throws Exception {
    ​
            SecurityManager securityManager = getSecurityManager();
            if (securityManager == null) {
                String msg = "SecurityManager property must be set.";
                throw new BeanInitializationException(msg);
            }
    ​
            if (!(securityManager instanceof WebSecurityManager)) {
                String msg = "The security manager does not implement the WebSecurityManager interface.";
                throw new BeanInitializationException(msg);
            }
    ​
            FilterChainManager manager = createFilterChainManager();
            //Expose the constructed FilterChainManager by first wrapping it in a
            // FilterChainResolver implementation. The AbstractShiroFilter implementations
            // do not know about FilterChainManagers - only resolvers:
            PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
            chainResolver.setFilterChainManager(manager);
    ​
            Map<String, Filter> filterMap = manager.getFilters();
            Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
            if (invalidRequestFilter instanceof InvalidRequestFilter) {
                //此处是关键,设置false跳过URL携带中文400,servletPath中文校验bug
                ((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
            }
            //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
            //FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class
            //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
            //injection of the SecurityManager and FilterChainResolver:
            return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
        }
    ​
        private static final class MySpringShiroFilter extends AbstractShiroFilter {
            protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
                if (webSecurityManager == null) {
                    throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
                } else {
                    this.setSecurityManager(webSecurityManager);
                    if (resolver != null) {
                        this.setFilterChainResolver(resolver);
                    }
    ​
                }
            }
        }
    ​
    }
    @Component
    @Slf4j
    public class JwtFilter extends BasicHttpAuthenticationFilter {
    ​
        /**
         * 默认开启跨域设置(使用单体)
         * 微服务情况下,此属性设置为false
         */
        private boolean allowOrigin = true;
    ​
        public JwtFilter(){}
        public JwtFilter(boolean allowOrigin){
            this.allowOrigin = allowOrigin;
        }
    ​
        /**
         * 执行登录认证
         *
         * @param request
         * @param response
         * @param mappedValue
         * @return
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                throw new AuthenticationException("Token失效,请重新登录", e);
            }
        }
    ​
        /**
         *
         */
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = httpServletRequest.getHeader(UserCommonConstant.ACCESS_TOKEN);
            if (StringUtils.isEmpty(token)) {
                token = httpServletRequest.getParameter(UserCommonConstant.ACCESS_TOKEN);
            }
    ​
            JwtToken jwtToken = new JwtToken(token);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(request, response).login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }
    ​
    ​
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    ​
            if(allowOrigin){
                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"));
                httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            }
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
    ​
            return super.preHandle(request, response);
        }
    }

  4. 登录接口优化

        @ApiOperation("登录")
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        public JSONResponse login(@RequestBody JwtAuthenticationRequest jwtAuthenticationRequest) throws Exception {
            String username = jwtAuthenticationRequest.getUsername();
            String password = jwtAuthenticationRequest.getPassword();
            //1. 验证校验码
            Object sessionCode = redisUtil.get(String.format(RedisKeyConstant.REDIS_KEY_CAPTCHA, jwtAuthenticationRequest.getUuid()));
            if (Objects.isNull(sessionCode)) {
                throw new UserInvalidException("验证码过期");
            }
            if (Objects.isNull(jwtAuthenticationRequest.getVerCode()) || !sessionCode.equals(jwtAuthenticationRequest.getVerCode().trim().toLowerCase())) {
                throw new UserInvalidException("验证码不正确");
            }
            //2. 获取一个主题,将来用于封装用户的用户信息(登录成功后),该subject,shiro会自动管理
            LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysUser::getUsername, username);
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            String userPassword = PasswordUtil.encrypt(username, password, sysUser.getSalt());
            String sysPassword = sysUser.getPassword();
            if (!sysPassword.equals(userPassword)) {
                if (redisUtil.incr("retryTimesKey:" + jwtAuthenticationRequest.getUsername(), 1L, 60) > 5) {
                    //更新账户状态
                    return super.toJSONString(1, "账号已锁定,请联系管理员");
                } else {
                    return super.toJSONString(1, "用户名或者密码错误");
                }
            }
            //3. 验证账户合法性,省略
    ​
            //4. 生成token
            String token = JwtUtil.sign(sysUser.getUsername(), sysUser.getPassword());
            redisUtil.set(RedisKeyConstant.PREFIX_USER_TOKEN + token, token);
            redisUtil.expire(RedisKeyConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
    ​
            Map<String, Object> result = new HashMap<>();
            result.put("accessToken", token);
            result.put("id", sysUser.getId());
            result.put("userInfo", sysUserService.queryUserAuth(sysUser.getUsername()));
            result.put("perm", sysPermissionService.queryUserPermission(sysUser.getUsername()));
    ​
            return RestUtils.returnRes(true, this, result);
        }