亲宝软件园·资讯

展开

Spring boot整合shiro+jwt实现前后端分离

人气:0

这里内容很少很多都为贴的代码,具体内容我经过了看源码和帖子加了注释。帖子就没用太多的内容

先下载shiro和jwt的jar包

<!-- shiro包 -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.4.0</version>
</dependency>
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <version>1.4.0</version>
</dependency>
<!--JWT依赖-->
<!--JWT-->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.4.0</version>
</dependency>
<!--JJWT-->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>

创建shiro的自定义的Realm

代码如下:

package com.serverprovider.config.shiro.userRealm;
 
 
import com.spring.common.auto.autoUser.AutoUserModel;
import com.spring.common.auto.autoUser.extend.AutoModelExtend;
import com.serverprovider.config.shiro.jwt.JWTCredentialsMatcher;
import com.serverprovider.config.shiro.jwt.JwtToken;
import com.serverprovider.service.loginService.LoginServiceImpl;
import com.util.Redis.RedisUtil;
import com.util.ReturnUtil.SecretKey;
import com.util.encryption.JWTDecodeUtil;
import io.jsonwebtoken.Claims;
import org.apache.log4j.Logger;
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.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ExceptionHandler;
 
import java.util.HashSet;
import java.util.List;
import java.util.Set;
 
public class UserRealm extends AuthorizingRealm {
 
  private Logger logger = Logger.getLogger(UserRealm.class);
 
 
  @Autowired private LoginServiceImpl loginService;
 
 
  public UserRealm(){
    //这里使用我们自定义的Matcher验证接口
    this.setCredentialsMatcher(new JWTCredentialsMatcher());
  }
 
  /**
   * 必须重写此方法,不然Shiro会报错
   */
  @Override
  public boolean supports(AuthenticationToken token) {
    return token instanceof JwtToken;
  }
 
  /**
   * shiro 身份验证
   * @param token
   * @return boolean
   * @throws AuthenticationException 抛出的异常将有统一的异常处理返回给前端
   *
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {
    /**
     * AuthenticationToken
     * JwtToken重写了AuthenticationToken接口 并创建了一个接口token的变量
     *  因为在filter我们将token存入了JwtToken的token变量中
     *  所以这里直接getToken()就可以获取前端传递的token值
     */
      String JWTtoken = ((JwtToken) token).getToken();
    /**
     * Claims对象它最终是一个JSON格式的对象,任何值都可以添加到其中
     * token解密 转换成Claims对象
     */
       Claims claims = JWTDecodeUtil.parseJWT(JWTtoken, SecretKey.JWTKey);
      
    /**
     *  根据JwtUtil加密方法加入的参数获取数据
     *  查询数据库获得对象
     *  如为空:抛出异常
     *  如验证失败抛出 AuthorizationException
     */
      String username = claims.getSubject();
      String password = (String) claims.get("password");
      AutoModelExtend principal = loginService.selectLoginModel(username,password);
      return new SimpleAuthenticationInfo(principal, JWTtoken,"userRealm");
  }
 
 
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    SimpleAuthorizationInfo info = null;
    /**
     * PrincipalCollection对象
     * 文档里面描述:返回从指定的Realm 仅作为Collection 返回的单个Subject的对象,如果没有来自该领域的任何对象,则返回空的Collection。
     * 在登录接口放入权限注解返回的错误信息:Subject.login(AuthenticationToken)或SecurityManager启用'Remember Me'功能后成功自动获取这些标识主体
     * 当调用Subject.login()方法成功后 PrincipalCollection会自动获得该对象 如没有认证过或认证失败则返回空的Collection并抛出异常
     * getPrimaryPrincipal():返回在应用程序范围内使用的主要对象,以唯一标识拥有帐户。
     */
    Object principal = principals.getPrimaryPrincipal();
    /**
     * 得到身份对象
     * 查询该用户的权限信息
     */
    AutoUserModel user = (AutoUserModel) principal;
    List<String> roleModels = loginService.selectRoleDetails(user.getId());
    try {
    /**
     * 创建一个Set,来放置用户拥有的权限
     * 创建 SimpleAuthorizationInfo, 并将办好权限列表的Set放入.
     */
    Set<String> rolesSet = new HashSet();
    for (String role : roleModels) {
      rolesSet.add(role);
    }
    info = new SimpleAuthorizationInfo();
    info.setStringPermissions(rolesSet);  // 放入权限信息
  }catch (Exception e){
    throw new AuthenticationException("授权失败!");
  }
    return info;
  }
}

这个授权方法遇到的坑比较少,就是在最终验证的时候网上很照抄过来的帖子一点都没有验证就粘贴赋值,在这里严重吐槽。

在使用jwt最为token而取消shiro传统的session时候,我们的需要重写shiro的验证接口   CredentialsMatcher,在 自定义的realm

中我们加入我们重写的验证方法,在调用SimpleAuthenticationInfo()方法进行验证的时候,shiro就会使用重写的验证接口。

此处为大坑。

贴上代码如下:

import com.spring.common.auto.autoUser.extend.AutoModelExtend;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
 
 
 
/**
 * CredentialsMatcher
 * 接口由类实现,该类可以确定是否提供了AuthenticationToken凭证与系统中存储的相应帐户的凭证相匹配。
 * Shiro 加密匹配 重写匹配方法CredentialsMatcher 使用JWTUtil 匹配方式
 */
public class JWTCredentialsMatcher implements CredentialsMatcher {
 
  private Logger logger = Logger.getLogger(JWTCredentialsMatcher.class);
 
  /**
   * Matcher中直接调用工具包中的verify方法即可
   */
  @Override
  public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
    String token = (String) ((JwtToken)authenticationToken).getToken();
    AutoModelExtend user = (AutoModelExtend)authenticationInfo.getPrincipals().getPrimaryPrincipal();
    //得到DefaultJwtParser
    Boolean verify = JwtUtil.isVerify(token, user);
    logger.info("JWT密码效验结果="+verify);
    return verify;
  }
}

shiro的配置项  ShiroConfiguration代码如下:

import com.serverprovider.config.shiro.shiroSysFile.JwtFilter;
import com.serverprovider.config.shiro.userRealm.UserRealm;
import org.apache.log4j.Logger;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;
 
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.*;
 
@Configuration
public class ShiroConfiguration {
 
  private Logger logger = Logger.getLogger(ShiroConfiguration.class);
 
 
 
  @Bean("shiroFilter")
  public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    //拦截器
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    // 配置不会被拦截的链接 顺序判断
    filterChainDefinitionMap.put("/login/**", "anon");
    // 添加自己的过滤器并且取名为jwt
    Map<String, Filter> filterMap = new HashMap<String, Filter>();
    filterMap.put("jwt", new JwtFilter());
    shiroFilterFactoryBean.setFilters(filterMap);
    //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
    filterChainDefinitionMap.put("/**", "jwt");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
 
    return shiroFilterFactoryBean;
  }
 
  /**
   * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
   * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session
   */
  @Bean
  protected SessionStorageEvaluator sessionStorageEvaluator(){
    DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
    sessionStorageEvaluator.setSessionStorageEnabled(false);
    return sessionStorageEvaluator;
  }
 
 
  @Bean("securityManager")
  public SecurityManager securityManager(UserRealm userRealm) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm);
    /*
     * 关闭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);
 
    return securityManager;
  }
  /**
   * 创建自定义的UserRealm @bean
   */
  @Bean("userRealm")
  public UserRealm shiroRealm() {
    UserRealm shiroRealm = new UserRealm();
    return shiroRealm;
  }
 
 
  //自动创建代理,没有这个鉴权可能会出错
  @Bean
  public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    autoProxyCreator.setProxyTargetClass(true);
    return autoProxyCreator;
  }
  /**
   * 开启shiro aop注解支持.
   * 使用代理方式;所以需要开启代码支持;
   *
   * @param
   * @return
   */
  @Bean
  public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
  }

到这里shiro配置就全部完成了

下面开始配置jwt:

首先我们需要重写  AuthenticationToken接口 此接口的作用

负责把shiro中username,password生成用于验证的token的封装类

所有我们需要去实现这个接口,封装我们自己生成的JWT生成的token

import org.apache.shiro.authc.AuthenticationToken;
 
/**
 * AuthenticationToken: shiro中负责把username,password生成用于验证的token的封装类
 * 我们需要自定义一个对象用来包装token。
 */
public class JwtToken implements AuthenticationToken {
 
  private String token;
 
  public JwtToken(String token) {
    this.token = token;
  }
 
  public String getToken() {
    return token;
  }
 
  public void setToken(String token) {
    this.token = token;
  }
 
  @Override
  public Object getPrincipal() {
    return null;
  }
 
  @Override
  public Object getCredentials() {
    return null;
  }
}

因为我们是前后端分离项目所有我们不存在session验证之类的 所有每次请求都需要携带token,所以每次请求我们都需要去验证token的真实性,所有我们需要去实现  BasicHttpAuthenticationFilter过滤器  

BasicHttpAuthenticationFilter继承 AuthenticatingFilter 过滤器其能够自动地进行基于所述传入请求的认证尝试。此实现是每个基本HTTP身份验证规范的Java实现  , 通过此过滤器得到HTTP请求资源获取Authorization传递过来的token参数   获取subject对象进行身份验证

代码如下:

import com.alibaba.fastjson.JSONObject;
import com.serverprovider.config.shiro.jwt.JwtToken;
import com.util.Util.utilTime;
import org.apache.log4j.Logger;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.io.PrintWriter;
import java.util.concurrent.TimeUnit;
 
/**
 *
 * BasicHttpAuthenticationFilter继承 AuthenticatingFilter 过滤器
 * 其能够自动地进行基于所述传入请求的认证尝试。
 *    BasicHttpAuthenticationFilter 基本访问认证过滤器
 *   此实现是每个基本HTTP身份验证规范的Java实现
 *   通过此过滤器得到HTTP请求资源获取Authorization传递过来的token参数
 *   获取subject对象进行身份验证
 *
 *
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {
 
  Logger logger = Logger.getLogger(JwtFilter.class);
 
  /**
   * 应用的HTTP方法列表配置基本身份验证筛选器。
   * 获取 request 请求 拒绝拦截登录请求
   * 执行登录认证方法
   *
   * @param request
   * @param response
   * @param mappedValue
   * @return
   */
  @Override
  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    String requestURI = httpServletRequest.getRequestURI();
    if (requestURI.equals("/user/login/verifyUser") || requestURI.equals("/user/register")) {
      return true;
    } else {
      try {
        executeLogin(request, response);
        return true;
      } catch (Exception e) {
        e.printStackTrace();
        return false;
      }
    }
  }
 
 
 
  /**
   * Authorization携带的参数为token
   * JwtToken实现了AuthenticationToken接口封装了token参数
   * 通过getSubject方法获取 subject对象
   * login()发送身份验证
   *
   * 为什么需要在Filter中调用login,不能在controller中调用login?
   * 由于Shiro默认的验证方式是基于session的,在基于token验证的方式中,不能依赖session做为登录的判断依据.
   * @param request
   * @param response
   */
  @Override
  protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    try{
      HttpServletRequest httpServletRequest = (HttpServletRequest) request;
      String token = httpServletRequest.getHeader("Authorization");
      JwtToken jwtToken = new JwtToken(token);
      // 提交给realm进行登入,如果错误他会抛出异常并被捕获
      Subject subject = getSubject(request, response);
      subject.login(jwtToken);
      logger.info("JWT验证用户信息成功");
      // 如果没有抛出异常则代表登入成功,返回true
      return true;
      }catch (Exception e){
      /* *
       * 这个问题纠结了好久
       *   原生的shiro验证失败会进入全局异常 但是 和JWT结合以后却不进入了 之前一直想不通
       *   原因是 JWT直接在过滤器里验证 验证成功与否 都是直接返回到过滤器中 成功在进入controller
       *   失败直接返回进入springboot自定义异常处理页面
       */
      JSONObject responseJSONObject = new JSONObject();
      responseJSONObject.put("result","401");
      responseJSONObject.put("resultCode","token无效,请重新获取。");
      responseJSONObject.put("resultData","null");
      responseJSONObject.put("resultTime", utilTime.StringDate());
      PrintWriter out = null;
      httpServletResponse.setCharacterEncoding("UTF-8");
      httpServletResponse.setContentType("application/json; charset=utf-8");
      logger.info("返回是");
      logger.info(responseJSONObject.toString());
      out = httpServletResponse.getWriter();
      out.append(responseJSONObject.toString());
    }
  return false;
  }
 
 
 
  /**
   * 对跨域提供支持
   */
  @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);
  }
 
}

贴上 加密解密校验的工具类

import com.spring.common.auto.autoUser.extend.AutoModelExtend;
import com.util.ReturnUtil.SecretKey;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;;
 
/* @author hw
 * @create 2019-04-16 10.12
 * @desc JWT工具类
 **/
 
public class JwtUtil {
 
 
  /**
   * 用户登录成功后生成Jwt
   * 使用Hs256算法 私匙使用用户密码
   *
   * @param ttlMillis jwt过期时间
   * @param user   登录成功的user对象
   * @return
   */
  public static String createJWT(long ttlMillis, AutoModelExtend user) {
    //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    //生成JWT的时间
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);
 
    //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("id", user.getId());
    map.put("username", user.getAuto_username());
    map.put("password", user.getAuto_password());
 
    //生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,
    // 切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
    String key = SecretKey.JWTKey;
    //生成签发人
    String subject = user.getAuto_username();
 
 
    //下面就是在为payload添加各种标准声明和私有声明了
    //这里其实就是new一个JwtBuilder,设置jwt的body
    JwtBuilder builder = Jwts.builder()
        //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
        .setClaims(map)
        //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
        .setId(UUID.randomUUID().toString())
        //iat: jwt的签发时间
        .setIssuedAt(now)
        //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
        .setSubject(subject)
        //设置签名使用的签名算法和签名使用的秘钥
        .signWith(signatureAlgorithm, key);
    if (ttlMillis >= 0) {
      long expMillis = nowMillis + ttlMillis;
      Date exp = new Date(expMillis);
      //设置过期时间
      builder.setExpiration(exp);
    }
    return builder.compact();
  }
 
 
  /**
   * 校验token
   * 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过
   *
   * @param token
   * @return
   */
  public static Boolean isVerify(String token, AutoModelExtend userModelExtend) {
    try {
      //得到DefaultJwtParser
      Claims claims = Jwts.parser()
          //设置签名的秘钥
          .setSigningKey(SecretKey.JWTKey)
          //设置需要解析的jwt
          .parseClaimsJws(token).getBody();
 
      if (claims.get("password").equals(userModelExtend.getAuto_password())) {
        return true;
      }
    } catch (Exception exception) {
      return false;
    }
    return null;
 
  }
 /**
   * Token的解密
   * @param token 加密后的token
   * @param secret 签名秘钥,和生成的签名的秘钥一模一样
   * @return
   */
  public static Claims parseJWT(String token, String secret) {
    //得到DefaultJwtParser
    Claims claims = Jwts.parser()
        //设置签名的秘钥
        .setSigningKey(secret)
        //设置需要解析的jwt
        .parseClaimsJws(token).getBody();
    return claims;
  }
}

到这里shiro jwt整合就完成了。

您可能感兴趣的文章:

加载全部内容

相关教程
猜你喜欢
用户评论