基础
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,重写addCorsMappings、addInterceptors,并注入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,定义变量Set<String>roleSet、Set<String> 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,需要大量的网络传输