SpringBoot 集成Shiro(二)

379 阅读4分钟

前言

上一篇文章讲解了Spring Boot集成shiro采用用户名和密码实现登录和权限验证,在前后端分离的环境下都采用的时Token方式,本文将讲解Spring Boot集成Shiro+JWT实现登录认证。

集成Shiro

导入jar包

  <dependency>
	  <groupId>org.springframework.boot</groupId>
	  <artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	
	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
	
	<dependency>
       <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-starter</artifactId>
        <version>1.7.1</version>
     </dependency>

        <dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>${mybatis-plus.version}</version>
		</dependency>
		
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<scope>runtime</scope>
	</dependency>
	
	<!--druid -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>${druid.version}</version>
		</dependency>
		
     <dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt</artifactId>
		<version>${jwt.version}</version>
	</dependency>   

自定义认证token

public class JwtToken implements AuthenticationToken
{
    private static final long serialVersionUID = -773194305699843478L;

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

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

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

自定义Realm实现认证

public class JWTTokenRealm extends AuthorizingRealm 
{
    private Logger logger = LoggerFactory.getLogger(JWTTokenRealm.class);
    
    @Autowired
    private UserService userService;
    
    //需重写此方法,不然Shiro会报错
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    
    
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)
    {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        User user = (User) principalCollection.getPrimaryPrincipal();
        Integer userOid=user.getOid();
        List<Role> roleList= userService.getRolesByUserOid(userOid);
        
        //用户角色
        Set<String> roleSet=new HashSet<>();
        //权限信息
        Set<String> funcSet=new HashSet<>();
        
        Set<Integer> roleOids=new HashSet<>();
        
        //查询角色
        if(roleList!=null && !roleList.isEmpty())
        {
            roleList.stream().forEach(t->{
                roleSet.add(String.valueOf(t.getRoleId()));
                roleOids.add(t.getOid());
            });
        }
        
        //查询权限
        List<Func> funcList= userService.getResByRoleOid(roleOids);
        if(funcList!=null && !funcList.isEmpty()){
            
            for(Func func:funcList)
            {
                funcSet.add(func.getUrl());
            }
        }
        
        //用户角色
        info.addRoles(roleSet);
        
        //用户权限
        info.addStringPermissions(funcSet);
        
        return info;
    }

    //用户认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authToken) throws AuthenticationException
    {
        //获取token值
        String accessToken = (String) authToken.getPrincipal();
        logger.info("AuthenticationInfo accessToken:{}",accessToken);
        
        //验证token是否有效
        Claims claims = JwtUtil.getClaims(accessToken);
        String userId = "";
        if (claims == null)
        {
            throw new RuntimeException("token失效,请重新登录");
        }
        else
        {
            userId = claims.getSubject();
            // 验证主题
            if (StringUtils.isEmpty(userId))
            {
                throw new RuntimeException("token失效,请重新登录");
            }
        }
        
        //查询用户信息
        User user = userService.getUserByUserId(userId);
        
        if(user==null)
        {
            throw new RuntimeException("用户名或密码不正确");  
        }
        
        //账号状态验证
        if("0".equals(user.getActive()))
        {
            throw new LockedAccountException("用户账号状态不正确");
        }
        
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
        return info;
    }
}

说明:从shiro中获取登录的token,验证token是否有效。

JWT工具类

public final class JwtUtil
{
    private static final String JWT_SCRET_KEY = "123#@456*";

    private String secret;
    private long expire;
    private String header;

    public static String createJWT(String subject, String issue, Object claim,
            long ttlMillis)
    {
        long nowMillis = System.currentTimeMillis();
        long expireMillis = nowMillis + ttlMillis;
        String result = Jwts.builder().setSubject(subject).setIssuer(issue)
                .setExpiration(new Date(expireMillis)).claim("user", claim).setId(issue)
                .signWith(getSignatureAlgorithm(), getSignedKey())
                .compressWith(CompressionCodecs.DEFLATE).compact();

        return result;
    }

    public static Jws<Claims> pareseJWT(String jwt)
    {
        Jws<Claims> claims;
        try
        {
            claims = Jwts.parser().setSigningKey(getSignedKey())
                    .parseClaimsJws(jwt);
        }
        catch (Exception ex)
        {
            claims = null;
        }
        return claims;
    }

    public static Claims getClaims(String jwt)
    {
        Claims claims;
        try
        {
            claims = Jwts.parser().setSigningKey(getSignedKey())
                    .parseClaimsJws(jwt).getBody();
        }
        catch (Exception ex)
        {
            claims = null;
        }
        return claims;
    }

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

    /**
     * 获取密钥
     * 
     * @return Key
     */
    private static Key getSignedKey()
    {
        byte[] apiKeySecretBytes = DatatypeConverter
                .parseBase64Binary(getAuthKey());
        Key signingKey = new SecretKeySpec(apiKeySecretBytes,
                getSignatureAlgorithm().getJcaName());
        return signingKey;
    }

    private static SignatureAlgorithm getSignatureAlgorithm()
    {
        return SignatureAlgorithm.HS256;
    }

    public static String getAuthKey()
    {
        String auth = JWT_SCRET_KEY;
        return auth;
    }

    public String getSecret()
    {
        return secret;
    }

    public void setSecret(String secret)
    {
        this.secret = secret;
    }

    public long getExpire()
    {
        return expire;
    }

    public void setExpire(long expire)
    {
        this.expire = expire;
    }

    public String getHeader()
    {
        return header;
    }

    public void setHeader(String header)
    {
        this.header = header;
    }
  }

自定义认证过滤器

public class JWTFilter extends AuthenticatingFilter
{
    private Logger logger = LoggerFactory.getLogger(JWTFilter.class);
    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest)
    {
        //从header中获取token
        String token = httpRequest.getHeader("token");

        //如果header中不存在token,则从参数中获取token
        if(StringUtils.isEmpty(token)){
            token = httpRequest.getParameter("token");
        }

        return token;
    }

    @Override
    protected AuthenticationToken createToken(ServletRequest request,
            ServletResponse response) throws Exception
    {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);

        if(StringUtils.isEmpty(token)){
            return null;
        }
        return new JwtToken(token);
    }
    
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse response, Object mappedValue) 
    {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
      
        String uri = request.getRequestURI();
        logger.info("start TokenInterceptor preHandle.." + uri);
        
        //过滤无需认证的请求 
        if (SystemUtil.isFree(uri) || SystemUtil.isProtected(uri))
        {
            return true;
        }
        
        //过滤options方法
        if(request.getMethod().equals(RequestMethod.OPTIONS.name()))
        {
            return true;
        }
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest,
            ServletResponse response) throws Exception
    {
        //获取请求token,如果token不存在,直接返回401
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = getRequestToken(request);
        
        if(StringUtils.isEmpty(token))
        {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
            httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            
            Map<String, Object> map =new HashMap<String, Object>();
            map.put("code", 401);
            map.put("msg", "invalid token");
            String json=JSON.toJSONString(map);
            httpResponse.getWriter().print(json);
            return false;
        }
         //认证成功调用登录接口
        return executeLogin(request, response);
    }
    
    /**
     * 登录失败
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest servletRequest, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Map<String, Object> map =new HashMap<String, Object>();
            map.put("code", 401);
            map.put("msg",throwable);
            String json=JSON.toJSONString(map);
            httpResponse.getWriter().print(json);
        } 
        catch (IOException e1) 
        {
            logger.error("onLoginFailure error",e);
        }
        return false;
    }

}

1.过滤器方法执行顺序为:preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin 。 2.isAccessAllowed 认证时需要对特殊的请求进行过滤 3.onAccessDenied 认证失败,可以自定义提示信息

shiro的核心配置

@Configuration
public class ShiroConfig
{
    @Bean
    public JWTTokenRealm tokenRealm()
    {
        JWTTokenRealm tokenRealm=new  JWTTokenRealm();
        return tokenRealm;
    }
    
    @Bean
    public DefaultWebSecurityManager securityManager() 
    {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(tokenRealm());
        securityManager.setRememberMeManager(null);
        return securityManager;
    }
    
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager)
    {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给filter设置安全管理
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        
        //添加jwt过滤
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filters);
        
        //配置系统的受限资源
        Map<String,String> map = new HashMap<>();
        
        /**
         *  anon: 无需认证(登录)可以访问
         *  authc: 必须认证才可以访问
         *  user: 如果使用rememberMe的功能可以直接访问
         *  perms: 该资源必须得到资源权限才可以访问
         *  role: 该资源必须得到角色权限才可以访问
         */
         
        //登录请求无需认证
        map.put("/login", "anon");
        //其他所有请求需要经过jwt认证
        map.put("/**", "jwt");
        
        //访问需要认证的页面,如果未登录会跳转到/unLogin
        shiroFilterFactoryBean.setLoginUrl("/unLogin");
        //访问未授权页面会自动跳转到/unAuth
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
    
    
    /**
     * 开启注解方式,页面可以使用注解
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }
}

说明:注意需要添加认证的过滤器,且所有的请求都需要过滤器验证

 //添加过滤器
 Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filters);

登录验证

   @RequestMapping("/login")
   @ResponseBody
   public String login(@RequestParam String userId,@RequestParam String password)
   {
     User user = new User();
     user.setUserId(userId);
     user.setPwd(password);
     loginService.authUser(user);
     //设置有效时间
     Long expreTime =86400000l;
     String tokenKey = UUID.randomUUID().toString();
     String tokenId = JwtUtil.createJWT(user.getUserId(), tokenKey,
             user.getUserId(), expreTime);
     //返回给前端
     Map<String, String> map = new HashMap<>();
     map.put("token", tokenId);
     logger.info("login token----->:{}",tokenId);
     return JSON.toJSONString(map);
   }

测试

授权测试

用户未登录,直击访问/app/sys/user/list,则页面会提示401错误

图片.png

用户登录测试

用户访问/login请求输入正确的用户名和密码,登录成功后将返回token

图片.png

token错误验证

用户访问/app/sys/user/resourceTest,header中传入的token值不正确

图片.png

角色、权限测试

角色和权限测试与第一章的方法相同。

总结

本文讲解了Spring Boot shiro与JWT实现登录验证,如有问题可以随时解答。