shiro+jwt实现RBAC(密码/验证码)

基础

1.支持使用手机号+密码或者手机号+验证码的方式登陆
2.需要使用redis缓存验证码、请求次数和用户信息
3.使用MybatisPlus操作mysql数据库,MybatisPlus提供了BaseMapper定义了简单的增删改查,提供了IServise定义了简单的用户操作,提供了MetaObjectHandler实现公共数据的自动填充
4.数据库(RBAC)
权限表、角色表、用户表、角色-权限表、用户-角色表
5.实体类(entity/pojo)
权限类Perm、角色类Role、用户类User
User中

@JsonIgnore
private String salt; //盐值,用于加密
@JsonIgnore
private Integer flag;//用户是否已删除

6.Mapper(继承MybatisPlus的BaseMapper)
①权限PermMapper:

Set<String> getPermissionsSet(@Param("roleId") Integer roleId); //根据角色ID获取权限;

②角色RoleMapper:

Set<String> getRoleSet(@Param("userId") Integer userId); //根据用户id获取其角色
@Select("SELECT role_id FROM sys_user_role WHERE user_id = #{userId}")
Set<Integer> getRoleIdSet(Integer userId); //根据用户id获取其角色ID
void addRole(@Param("userId") Integer userId, @Param("roleIds") Integer[] roleIds); //给用户添加角色

③用户UserMapper
编写PermMapper.xml和RoleMapper.xml。
7.service
①IUserService继承MybatisPlus的IService

User selectUserByPhone(String phone);//根据手机号查询用户
Boolean register(String phone, String... args); // 用户注册,默认密码为手机号后六位

②UserServiceImpl继承MybatisPlus的ServiceImpl,实现IUserService,重写selectUserByPhone和register方法。
其中register方法中使用事务向数据库中插入一条数据。

return transactionTemplate.execute(status -> {
   try {
      userMapper.insert(user);//用户表
      permissionsService.addRole(user.getUserId(),RoleEnums.COMMON.getCode());//用户角色表
      return Boolean.TRUE;
   }
   catch (Exception e) {
      // 回滚
      status.setRollbackOnly();
      return Boolean.FALSE;
   }
});

工具类

1.PhoneNumberValidator类,实现java的ConstraintValidator接口,判断手机号是否合法
2. RequestLimit注解,限制second内访问次数maxCount
3. Constant类,定义验证码过期时间 CODE_EXPIRE_TIME、jwtToken过期时间TOKEN_EXPIRE_TIME、jwtToken刷新时间TOKEN_REFRESH_TIME 、token请求头名称TOKEN_HEADER_NAME
4.RedisKey类,定义getLoginCodeKey(String phone)、getModifyPasswordCodeKey(String phone)获取redis中缓存的验证码
getLoginUserKey(String phone)获取该phone对应的LoginUser对象
getRequestLimitKey(String servletPath,String phone)获取该phone访问某servletPath的次数。
都是在phone前面加上前缀后返回(例如getLoginUserKey 在phone前面加上LOGIN:USER:)
5. ErrorState枚举类,定义错误返回信息
6. RoleEnums枚举类,定义角色信息
7. CommonsUtils类,getCode()随机生成6位数验证码,encryptPassword()使用SHA256对密码加密
8. ServletUtils类,getRequest()获取request,getResponse()获取response,getSession()获取session,getRequestAttributes()获取request属性,renderString()将字符串添加到response。
9. Threads类,shutdownAndAwaitTermination(ExecutorService pool),停止线程池(先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. 如果超时, 则调用shutdownNow,取消在workQueue中Pending的任务,并中断所有阻塞函数,如果仍然超时,则强制退出)

if (pool != null && !pool.isShutdown()) {
    pool.shutdown();
    try {
        if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
            pool.shutdownNow();
            if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
                logger.info("Pool did not terminate");
            }
        }
    } catch (InterruptedException ie) {
        pool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

printException() 打印线程异常信息
10. AopLog类,日志切面

@Aspect
@Component
@Slf4j
public class AopLog {
   private static final String START_TIME = "request-start";
   //切入点
   @Pointcut("execution(public * com.learn.project.project.controller.*Controller.*(..))")
   public void pointCut() {
   }
   //前置操作,point 切入点
   @Before("pointCut()")
   public void beforeLog(JoinPoint point) {
      HttpServletRequest request = ServletUtils.getRequest();

      log.info("【请求 URL】:{}", request.getRequestURL());
      log.info("【请求 IP】:{}", request.getRemoteAddr());
      log.info("【请求类名】:{},【请求方法名】:{}", point.getSignature().getDeclaringTypeName(), point.getSignature().getName());
      Map<String, String[]> parameterMap = request.getParameterMap();
      log.info("【请求参数】:{},", JSONUtil.toJsonStr(parameterMap));
      Long start = System.currentTimeMillis();
      request.setAttribute(START_TIME, start);
   }
   //环绕操作,point 切入点,返回原方法返回值
   @Around("pointCut()")
   public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
      Object result = point.proceed();
      log.info("【返回值】:{}", JSONUtil.toJsonStr(result));
      return result;
   }
   //后置操作
   @AfterReturning("pointCut()")
   public void afterReturning() {
      ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
      Long start = (Long) request.getAttribute(START_TIME);
      Long end = System.currentTimeMillis();
      log.info("【请求耗时】:{}毫秒", end - start);
   }
}

11.返回结果类Result,返回消息success(…)/error(…)
12. GlobalExceptionAdvice类

@RestControllerAdvice //处理所有被@RequestMapping注解的类或方法中的异常
public class GlobalExceptionAdvice {
   @ExceptionHandler(AuthorizationException.class) //要处理的异常
   public ResponseEntity<String> handleShiroException() { //shiro权限异常处理
   return ResponseEntity.status(HttpStatus.UNAUTHORIZED).contentType(MediaType.APPLICATION_JSON).body(JSONUtil.toJsonStr(Result.error(ErrorState.NOT_AUTH)));
   }//非法参数异常处理、运行时异常、参数校验异常、token无效异常、通用业务异常
}

13.ServiceException类,继承RunTimeException类
14. RequestLimitInterceptor类,实现HandlerInterceptor类,重写preHandle方法,实现请求限制。

public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response, Object handler) {
    log.info("接口请求限制拦截器执行了...");
    if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {
    	// HandlerMethod 封装方法定义相关的信息,如类,方法,参数等
    	HandlerMethod handlerMethod = (HandlerMethod) handler;
    	Method method = handlerMethod.getMethod();
    	RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class); // 获取方法中是否包含注解
    	RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class); // 获取类中是否包含注解
    	RequestLimit requestLimit = methodAnnotation != null ? methodAnnotation : classAnnotation; // 如果方法上有注解就优先使用方法上的注解的参数,否则使用类上的
    	if (requestLimit != null) {
            if (isLimit(request, requestLimit)) {
        	ServletUtils.renderString(response, JSONUtil.toJsonStr(Result.error(requestLimit.msg()))); // 返回请求限制错误
        	return false;
            }
    	}
    }
    return true;
}

isLimit方法,使用RedisTemplate从redis中读取请求次数,并判断是否超过最大次数限制
15. AsyncManager类,异步任务管理器
单例饿汉式创建,并声明异步操作任务调度线程池,并定义execute()执行任务和shutdown()停止线程池

private static final AsyncManager me = new AsyncManager();//饿汉式
private AsyncManager() {
}
public static AsyncManager me() {
   return me;
}//单例模式
private final int OPERATE_DELAY_TIME = 10; //操作延迟10毫秒
private final ScheduledExecutorService executor = SpringUtil.getBean("scheduledExecutorService");//异步操作任务调度线程池
public void execute(TimerTask task) {//执行任务
   executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}
public void shutdown() {//停止任务线程池
   Threads.shutdownAndAwaitTermination(executor);
}

16.AppShutDownManager,定义shutdownAsyncManager()方法,调用AsyncManager.me().shutdown()确保关闭后台任务线程池
17. BaseController类,定义响应返回结果
protected Result result(int rows) {
return rows > 0 ? Result.success() : Result.error();
}
protected Result result(boolean flag) {
return flag ? Result.success() : Result.error();
}
18. CustomizeMetaObjectHandler 实现mybatisplus的MetaObjectHandler,重写inserFill和uppdateFill,实现创建时间和更新时间公共字段的自动填充

配置类

1.MvcConfig类,实现WebMvcConfigurer,重写addCorsMappingsaddInterceptors,并注入requestLimitInterceptor
2. SwaggerConfig类
3. ThreadPoolConfig类,线程池配置

@Configuration
public class ThreadPoolConfig {
   // 核心线程数:默认值为1 参数描述:当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程
    private final int corePoolSize = 5;
   /*最大可创建的线程数:默认值为Integer.MAX_VALUE 参数描述:线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务*/
   private final int maxPoolSize = 200;
   //队列最大长度:默认值为Integer.MAX_VALUE 参数描述:存储任务的队列长度
   private final int queueCapacity = 1000;
   /*线程池维护线程所允许的空闲时间:默认值为60s 参数描述:当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程*/
   private final int keepAliveSeconds = 300;
   /*默认值:false 参数描述:设置为true时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭,通常是不必要的 */
   private final boolean setAllowCoreThreadTimeOut = false;
   /*参数描述:当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略 四种拒绝策略:
AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。 
DiscardPolicy丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。 
DiscardOldestPolicy 丢弃队列最前面的任务,然后重新提交被拒绝的任务。 CallerRunsPolicy 由调用线程处理该任务 */
   private final RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();
   @Bean
   public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
      ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
      executor.setCorePoolSize(corePoolSize);
      executor.setMaxPoolSize(maxPoolSize);
      executor.setQueueCapacity(queueCapacity);
      executor.setKeepAliveSeconds(keepAliveSeconds);
      executor.setRejectedExecutionHandler(rejectedExecutionHandler);
      executor.setAllowCoreThreadTimeOut(setAllowCoreThreadTimeOut);
      return executor;
   }
   //定时任务线程池
   @Bean
   protected ScheduledExecutorService scheduledExecutorService() {
      return new ScheduledThreadPoolExecutor(corePoolSize);
   }
}

关键核心

1.登陆用户类LoginUser,定义变量SetroleSet、Set permissionsSet、User user。
2.用户名(手机号作为用户名)密码使用UsernamePasswordToken,jwt使用BearerToken,手机号验证码需要自定义PhoneCodeToken实现shiro的AuthenticationToken接口,重写getPrincipal()和getCredentials()方法。

@Override
public Object getPrincipal() {
   return getPhone();
}
@Override
public Object getCredentials() {
   return getCode();
}

3.JwtFilter 继承shiro的BasicHttpAuthenticationFilter,重写isAccessAllowed、executeLogin、preHandle方法

// 执行登录认证
@Override    
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
     String token = ((HttpServletRequest) request).getHeader(Constant.TOKEN_HEADER_NAME);//从请求头获取token
     if (StrUtil.isNotBlank(token)) {//StrUtil是hutool中提供的
         return executeLogin(request, response);
     }
     // 如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true(即通过jwtFilter,去执行其他过滤条件)
     return true;
}
//执行登录
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
     HttpServletRequest httpServletRequest = (HttpServletRequest) request;
     String token = httpServletRequest.getHeader(Constant.TOKEN_HEADER_NAME);
     BearerToken bearerToken = new BearerToken(token);//BearerToken是shiro类
     // 提交给realm进行登入,如果错误他会抛出异常并被捕获
     try {
         getSubject(request, response).login(bearerToken);
     }catch (IncorrectCredentialsException e) {
         ServletUtils.renderString((HttpServletResponse) response,         JSONUtil.toJsonStr(Result.error(ErrorState.TOKEN_INVALID)));
         return false;
     }
     // 如果没有抛出异常则代表登入成功,返回true
     return true;
}
//对跨域提供支持
@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);
}

4.TokenService类,创建、验证token,并从token中取信息

// 获取当前登录的User对象,先从redis中取,无则从数据库中取并放入redis
public LoginUser getLoginUser() {
   String token = getToken(ServletUtils.getRequest());// 获取token
   String phone = getPhone(token);   // 获取手机号
   String loginUserKey = RedisKey.getLoginUserKey(phone); // 获取缓存loginUserKey
   LoginUser cacheObject = redisTemplate.opsForValue().get(loginUserKey); // 获取缓存loginUser
   if (cacheObject == null) {//redis缓存中没有
      LoginUser loginUser = new LoginUser();
      User user = userService.selectUserByPhone(phone); // 获取当前登录用户
      loginUser.setUser(user);
      Set<String> permissionsSet = permissionsService.getPermissionsSet(user.getUserId());// 获取当前登录用户所有权限
      loginUser.setPermissionsSet(permissionsSet);
      Set<String> roleSet = permissionsService.getRoleSet(user.getUserId());// 获取当前登录用户所有角色
      loginUser.setRoleSet(roleSet);
      redisTemplate.opsForValue().set(loginUserKey, loginUser, 15, TimeUnit.MINUTES); // 缓存当前登录用户
      return loginUser;
   }
   return cacheObject;
}
//获得token中的信息无需secret解密也能获得
public String getPhone(String token) {//从token中获取手机号
   try {
      DecodedJWT jwt = JWT.decode(token);
      return jwt.getClaim("phone").asString();
   }
   catch (JWTDecodeException e) {
      return null;
   }
}
public String getUserId(String token) { //从token中获取用户id
   try {
      DecodedJWT jwt = JWT.decode(token);
      return jwt.getClaim("userId").asString();
   }
   catch (JWTDecodeException e) {
      return null;
   }
}
//获取当前登录用户的token,如果token为null则获取refreshToken
public String getToken(HttpServletRequest request) {
   String token = request.getHeader(Constant.TOKEN_HEADER_NAME);
   if (StrUtil.isBlank(token)) {
      return request.getParameter("refreshToken");
   }
   else {
      return token;
   }
}
//根据用户名(手机号)、用户id、用户的密码生成token,设置有效时间并加密
public String createToken(String phone, Integer userId, String secret, Long time) {
   Date date = new Date(System.currentTimeMillis() + time);
   Algorithm algorithm = Algorithm.HMAC256(secret);
   return JWT.create().withClaim("phone", phone).withClaim("userId", String.valueOf(userId)).withExpiresAt(date).sign(algorithm);
}
//校验token是否正确
public boolean verify(String token, String secret) {//secret:密码
   try {
      // 根据密码生成JWT效验器
      Algorithm algorithm = Algorithm.HMAC256(secret);
      JWTVerifier verifier = JWT.require(algorithm).withClaim("phone", getPhone(token)).withClaim("userId", getUserId(token)).build();
      // 效验TOKEN
      verifier.verify(token);
      return true;
   }
   catch (JWTVerificationException exception) {
      return false;
   }
}

5.PermissionsService类中调用PermMapper和RoleMapper
getRoleSet(userId)根据用户id获取其角色列表
addRole(userId,roleIds)给用户添加角色
getPermissionsSet(userId)根据用户id获取权限列表
getPermissionsSetByRoleId(roleId)根据角色id获取权限列表

6.分别对用户名密码、手机号验证码和jwt定义realm,继承AuthorizingRealm,重写supports()、doGetAuthenticationInfo()、doGetAuthorizationInfo()

public class PasswordRealm extends AuthorizingRealm { 
	@Resource
	private IUserService userService;
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof UsernamePasswordToken;
	}
	// 获取授权信息
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		return null;
	}//只用于登陆,无需权限
	//获取身份认证信息
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) 	throws AuthenticationException {
		UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
		log.info(token.getUsername() + " - password auth start...");
		User user = userService.selectUserByPhone(token.getUsername());// 根据手机号查询用户
		if (user == null) {// 抛出异常账号不存在
			throw new UnknownAccountException();
		}
		// 1.principal:认证的实体信息,可以是手机号,也可以是数据表对应的用户的实体类对象
		// 2.credentials:密码
		Object credentials = user.getPassword();
		// 3.realmName:当前realm对象的name,调用父类的getName()方法即可
		String realmName = super.getName();
		// 4.盐,取用户信息中唯一的字段来生成盐值,避免由于两个用户原始密码相同,加密后的密码也相同(setSalt时使用UUID.randomUUID())
		ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
		return new SimpleAuthenticationInfo(user, credentials, credentialsSalt, realmName);
	}
}
public class CodeRealm extends AuthorizingRealm {
      @Resource
      private IUserService userService;
      @Resource
      private StringRedisTemplate stringRedisTemplate;//需要从redis取验证码,用于校验前端验证码
      @Override
      public boolean supports(AuthenticationToken token) {
         return token instanceof PhoneCodeToken;
      }
      //获取授权信息
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
         return null;
      }
      //获取身份认证信息
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
         PhoneCodeToken token = (PhoneCodeToken) authenticationToken;
         log.info(token.getPhone() + " - code auth start...");
         User user = userService.selectUserByPhone(token.getPhone());// 根据手机号查询用户
         if (user == null) {
            throw new UnknownAccountException();// 抛出账号不存在异常
         }
         // principal:认证的实体信息,可以是手机号,也可以是数据表对应的用户的实体类对象
         // 从redis中获取登录验证码
         Object credentials = stringRedisTemplate.opsForValue().get(RedisKey.getLoginCodeKey(user.getPhone()));
         if (credentials == null) {
            throw new ExpiredCredentialsException();//过期
         }
         String realmName = super.getName();
         ByteSource credentialsSalt = ByteSource.Util.bytes(token.getPhone());
         return new SimpleAuthenticationInfo(user, credentials, credentialsSalt, realmName);
      }
}
public class JwtRealm extends AuthorizingRealm {
      @Resource
      private TokenService tokenService;
      @Resource
      private IUserService userService;
      @Override
      public boolean supports(AuthenticationToken token) {
         return token instanceof BearerToken;
      }
      //只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
         LoginUser loginUser = tokenService.getLoginUser();
         SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
         // 添加角色
         authorizationInfo.addRoles(loginUser.getRoleSet());
         // 添加权限
         authorizationInfo.addStringPermissions(loginUser.getPermissionsSet());
         return authorizationInfo;
      }
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
         BearerToken bearerToken = (BearerToken) authenticationToken;
         String token = bearerToken.getToken();// 获取jwtToken
         String phone = tokenService.getPhone(token); // 获得phone
         log.info(phone + " - token auth start...");
         if (StrUtil.isBlank(phone)) {// 如果获取到的手机号为空
            throw new IncorrectCredentialsException();
         }
         User user = userService.selectUserByPhone(phone);
         if (user == null) {//查不到用户
            throw new IncorrectCredentialsException();
         }
         boolean verify = tokenService.verify(token, user.getPassword());//验证密码
         if (!verify) {
            throw new IncorrectCredentialsException();
         }
         return new SimpleAuthenticationInfo(token, token, getName());
      }
}

7.CustomModularRealmAuthenticator类继承ModularRealmAuthenticator,重写doMultiRealmAuthentication方法

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
   Realm finalRealm;
   // 匹配Realm名称
   if (token instanceof BearerToken) {
      finalRealm = SpringUtil.getBean(JwtRealm.class);
   }
   else if (token instanceof PasswordRealm) {
      finalRealm = SpringUtil.getBean(PasswordRealm.class);
   }
   else {
      finalRealm = SpringUtil.getBean(CodeRealm.class);
   }
   return super.doSingleRealmAuthentication(finalRealm, token);
}

8.LoginService类声明IUserService和TokenService,sendLoginCode(phone)将验证码保存到redis
loginByPassword用户密码登陆
loginByCode验证码登录

   public Result loginByPassword(String phone, String password) {
      Subject subject = SecurityUtils.getSubject();// 1.获取Subject
      UsernamePasswordToken token = new UsernamePasswordToken(phone, password); // 2.封装用户数据
      try {// 3.执行登录方法
         subject.login(token);
         return Result.success(returnLoginInitParam(phone));
      }
      catch (UnknownAccountException e) {
         return Result.error(ErrorState.USERNAME_NOT_EXIST);
      }
      catch (IncorrectCredentialsException e) {
         return Result.error(ErrorState.PASSWORD_ERROR);
      }
   }
   public Result loginByCode(String phone, String code) {
      Subject subject = SecurityUtils.getSubject();// 1.获取Subject
      User sysUser = userService.selectUserByPhone(phone);
      if (Objects.isNull(sysUser)) {// 2.验证码登录,如果该用户不存在则创建该用户
         userService.register(phone); // 2.1 注册
      }
      PhoneCodeToken token = new PhoneCodeToken(phone, code); // 3.封装用户数据
      try {// 4.执行登录方法
         subject.login(token);
         return Result.success(returnLoginInitParam(phone));
      }
      catch (UnknownAccountException e) {
         return Result.error(ErrorState.USERNAME_NOT_EXIST);
      }
      catch (ExpiredCredentialsException e) {
         return Result.error(ErrorState.CODE_EXPIRE);
      }
      catch (IncorrectCredentialsException e) {
         return Result.error(ErrorState.CODE_ERROR);
      }
   }
   public Result modifyPassword(String phone, String code, String password) {}//通过验证码修改密码

   //返回登录后初始化参数
   private Map<String, Object> returnLoginInitParam(String phone) {
      Map<String, Object> data = new HashMap<>(4);
      User user = userService.selectUserByPhone(phone); // 根据手机号查询用户
      String token = tokenService.createToken(phone, user.getUserId(), user.getPassword(), Constant.TOKEN_EXPIRE_TIME); // 生成jwtToken
      // 生成刷新token
      String refreshToken = tokenService.createToken(phone, user.getUserId(), user.getPassword(),Constant.TOKEN_REFRESH_TIME);
      // token
      data.put("token", token);
      // 刷新时所需token
      data.put("refreshToken", refreshToken);
      return data;
   }
   //token刷新(根据传递进来的refreshToken生成newToken和newRefreshToken)
   public Result tokenRefresh(String refreshToken) {
      String phone = tokenService.getPhone(refreshToken);
      User user = userService.selectUserByPhone(phone);
      boolean verify = tokenService.verify(refreshToken, user.getPassword());
      if (!verify) {
         return Result.error(ErrorState.REFRESH_TOKEN_INVALID);
      }
      Map<String, Object> data = new HashMap<>(4);
      String newToken = tokenService.createToken(phone, user.getUserId(), user.getPassword(),Constant.TOKEN_EXPIRE_TIME); // 生成jwtToken
      String newRefreshToken = tokenService.createToken(phone, user.getUserId(), user.getPassword(),Constant.TOKEN_REFRESH_TIME); // 生成刷新token
      // toke
      data.put("token", newToken);
      // 刷新时所需token
      data.put("refreshToken", newRefreshToken);
      return Result.success(data);
   }
}

9.LoginController类,继承BaseController,注入LoginService,

//发送验证码
@RequestLimit(second = 60 * 60 * 24, maxCount = 5) 
@GetMapping("/code")
public Result sendLoginCode(@PhoneNumber String phone) {
   return result(loginService.sendLoginCode(phone));
}
//密码登陆
@PostMapping("/password")
public Result loginByPassword(@PhoneNumber String phone, @NotEmpty(message = "密码不能为空") String password) {
   return loginService.loginByPassword(phone, password);
}
//验证码登录
@PostMapping("/code")
public Result loginByCode(@PhoneNumber String phone, @NotEmpty(message = "验证码不能为空") String code) {
   return loginService.loginByCode(phone, code);
}
//token刷新
@PostMapping("/token/refresh")
public Result tokenRefresh(@RequestParam String refreshToken) {
   return loginService.tokenRefresh(refreshToken);
}

10.UserController类,继承BaseController,注入IUserService和TokenService

//注册
@PostMapping("/register")
public Result register(@PhoneNumber String phone) {
   return super.result(userService.register(phone));
}
//获取当前用户基本信息
@GetMapping("/info")
public Result info() {
   LoginUser loginUser = tokenService.getLoginUser();
   return Result.success(loginUser);
}
@RequiresPermissions("system:user:remove")//需要有删除用户的权限
//删除用户
@DeleteMapping("/{userId}")
public Result deleted(@PathVariable @NotNull(message = "userId不能为空") Integer userId) {
   return super.result(userService.removeById(userId));
}

@RequiresPermissions("system:user:update") 
//修改用户
@PutMapping("/{userId}")
public Result update(User user) {
   return super.result(userService.updateById(user));
}
@RequiresPermissions(value = { "system:user:list" })
//获取所有用户
@GetMapping
public Result users() {
   return Result.success(userService.list());//list()是IService中已实现方法
}

@RequiresRoles(value = { "admin" })//用户需要有admin角色
//分页获取用户的admin角色的权限
@GetMapping("/page")
public Result user(@NotNull Integer pageNum, @NotNull Integer pageSize) {
   IPage<User> page = new Page<>(pageNum, pageSize);
   return Result.success(userService.page(page));
}

11.ShiroConfig类,shiro配置

@Configuration
@AutoConfigureBefore(value = ShiroAutoConfiguration.class)
public class ShiroConfig {
	//开启shiro权限注解
	@Bean
	public static DefaultAdvisorAutoProxyCreator creator() {
		DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
		creator.setProxyTargetClass(true);
		return creator;
	}
	//密码/验证码登录匹配器
	@Bean("hashedCredentialsMatcher")
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
		matcher.setHashAlgorithmName("SHA-256");// 设置哈希算法名称
		matcher.setHashIterations(1024);// 设置哈希迭代次数
		matcher.setStoredCredentialsHexEncoded(true);// 设置存储凭证十六进制编码
		return matcher;
	}
	//密码登录Realm
	@Bean
	public PasswordRealm passwordRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
		PasswordRealm userRealm = new PasswordRealm();
		userRealm.setCredentialsMatcher(matcher);
		return userRealm;
	}
	//验证码登录Realm
	@Bean
	public CodeRealm codeRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
		CodeRealm codeRealm = new CodeRealm();
		codeRealm.setCredentialsMatcher(matcher);
		return codeRealm;
	}
	//jwtRealm
	@Bean
	public JwtRealm jwtRealm() {
		return new JwtRealm();
	}
	/* Shiro内置过滤器,可以实现拦截器相关的拦截器。
常用的过滤器:
anon:无需认证(登录)可以访问;authc:必须认证才可以访问;user:如果使用rememberMe的功能可以直接访问;perms:该资源必须得到资源权限才可以访问;role:该资源必须得到角色权限才可以访问*/
	@Bean
	public ShiroFilterFactoryBean shiroFilter(@Qualifier("sessionsSecurityManager") SecurityManager securityManager) {
		ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
		// 设置 SecurityManager
		bean.setSecurityManager(securityManager);

		Map<String, String> filterMap = new LinkedHashMap<>();
		filterMap.put("/login/**", "anon");
		filterMap.put("/static/**", "anon");
		// 从这里开始,是我为解决问题增加的,为swagger页面放行
		filterMap.put("/swagger-ui.html", "anon");
		filterMap.put("/doc.html", "anon");
		filterMap.put("/swagger-resources/**", "anon");
		filterMap.put("/v2/**", "anon");
		filterMap.put("/webjars/**", "anon");
		filterMap.put("/images/**", "anon");

		Map<String, Filter> filter = new LinkedHashMap<>(1);
		filter.put("jwt", new JwtFilter());
		bean.setFilters(filter);//自定义一个jwt过滤器,将其加入shiro过滤器
		filterMap.put("/**", "jwt");//对所有请求通过jwt过滤器过滤
		bean.setFilterChainDefinitionMap(filterMap);
		return bean;
	}
	@Bean
	public Authenticator customAuthenticator() {
		// 自己重写的ModularRealmAuthenticator
		CustomModularRealmAuthenticator modularRealmAuthenticator = new CustomModularRealmAuthenticator();
		//多realm至少一个成功
		modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
		return modularRealmAuthenticator;
	}
	//SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(Subject)
	@Bean
	public SessionsSecurityManager sessionsSecurityManager(@Qualifier("passwordRealm") PasswordRealm passwordRealm, @Qualifier("codeRealm") CodeRealm codeRealm, @Qualifier("jwtRealm") JwtRealm jwtRealm, @Qualifier("customAuthenticator") Authenticator authenticator) {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		// 设置realm
		securityManager.setAuthenticator(authenticator);
		List<Realm> realms = new ArrayList<>();
		// 添加多个realm
		realms.add(passwordRealm);
		realms.add(codeRealm);
		realms.add(jwtRealm);
		securityManager.setRealms(realms);
		// 关闭shiro自带的session
		DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
		DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
		defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
		subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
		securityManager.setSubjectDAO(subjectDAO);
		return securityManager;
	}
}

总结

1.redis中保存验证码、LoginUser对象、某phone请求某servlet的次数
2.jwttoken只保存用户名(phone)、用户id和密码。
3.这种做法缺点:频繁访问redis,需要大量的网络传输

你可能感兴趣的:(java)