说起用户登录,就会涉及到有一个权限问题。因为用户他分普通用户和一些管理员用户之类的。简单的一些注解判断就可以处理好了。
白名单
package com.zzyl.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration
@ConfigurationProperties(prefix = "zzyl.framework.security")
@Data //提供get set 方法
public class SecurityProperties {
/**
* 白名单列表 --- 只要用户登录 , 无论有没有分配资源 , 都可以直接访问
*/
private List<String> publicAccessUrls = new ArrayList<>();
/**
* 白名单
*/
private List<String> ignoreUrl = new ArrayList<>();
}
用户登录
package com.zzyl.service.impl;
import cn.hutool.crypto.digest.BCrypt;
import cn.hutool.json.JSONUtil;
import com.zzyl.constant.CacheConstant;
import com.zzyl.constant.SuperConstant;
import com.zzyl.dto.LoginDto;
import com.zzyl.enums.BasicEnum;
import com.zzyl.exception.BaseException;
import com.zzyl.mapper.ResourceMapper;
import com.zzyl.mapper.UserMapper;
import com.zzyl.properties.JwtTokenManagerProperties;
import com.zzyl.properties.SecurityProperties;
import com.zzyl.service.LoginService;
import com.zzyl.utils.JwtUtil;
import com.zzyl.vo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
UserMapper userMapper;
@Autowired
JwtTokenManagerProperties jwtTokenManagerProperties;
@Autowired
ResourceMapper resourceMapper; //用于查询用户资源列表
@Autowired
SecurityProperties securityProperties; //用户获取白名单列表
@Autowired
RedisTemplate<String, String> redisTemplate; //用户存储redis数据
/**
* 登录
* @param loginDto
* @return
*/
@Override
public UserVo login(LoginDto loginDto) {
//1. 根据用户名查询用户
UserVo userVo = userMapper.selectByName(loginDto.getUsername());
//2. 校验用户是否存在 如果不存在则提示用户登录失败
if (userVo == null) {
throw new BaseException(BasicEnum.LOGIN_FAIL);
}
//3. 校验是否被禁用 如果被禁用 , 则提示账号被禁用 请联系管理员
if (userVo.getDataState().equals(SuperConstant.DATA_STATE_1)) {
throw new BaseException(BasicEnum.LOGIN_DISABLE);
}
//4. 校验密码是否正确 ,使用BCrypt 如果 不 正确则提示用户或密码错误
if (!BCrypt.checkpw(loginDto.getPassword(), userVo.getPassword())) {
throw new BaseException(BasicEnum.LOGIN_ERROR_PASSWORD);
}
//5. 密码脱敏
userVo.setPassword(null);
//获取用户列表
List<String> urls = resourceMapper.selectRequestPathByUserId(userVo.getId());
//获取白名单列表 在配置类里面 properties 包
List<String> publicAccessUrls = securityProperties.getPublicAccessUrls();
//合并
urls.addAll(publicAccessUrls);
//6. 生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put("currentUser", JSONUtil.toJsonStr(userVo));
String jwt = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(),
jwtTokenManagerProperties.getTtlback(),
claims);
//6.1 设置令牌
userVo.setUserToken(jwt);
//存储Redis 将用户所能访问的 , 所有的在资源列表
redisTemplate.opsForValue().set(CacheConstant.PUBLIC_ACCESS_URLS + userVo.getId(),
JSONUtil.toJsonStr(urls),
jwtTokenManagerProperties.getTtlback(),
TimeUnit.HOURS);
//7. 返回数据
return userVo;
}
}
登录拦截器
package com.zzyl.intercept;
import cn.hutool.core.text.AntPathMatcher;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.zzyl.constant.CacheConstant;
import com.zzyl.enums.BasicEnum;
import com.zzyl.exception.BaseException;
import com.zzyl.properties.JwtTokenManagerProperties;
import com.zzyl.utils.JwtUtil;
import com.zzyl.utils.UserThreadLocal;
import com.zzyl.vo.UserVo;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
//登录拦截器
@Component
public class UserTokenInterceptor implements HandlerInterceptor {
@Autowired
JwtTokenManagerProperties jwtTokenManagerProperties;
@Autowired
RedisTemplate<String, String> redisTemplate; //用户获取redis数据
//Spring路径匹配器AntPathMatcher
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 判断是否请求 controller中的方法 ,如果不是 直接放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
//2. 从请求头中获取令牌 , Authorization
String token = request.getHeader("Authorization");
//3. 校验令牌是否为空 , 为空返回401
if (StrUtil.isEmpty(token)) {
//权限不足
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
//4. 解析令牌, 并校验
Claims claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), token);
//如果claims为空 , 则抛异常
if (claims == null) {
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
//5. 获取用户数据 UserVo 返回object类型
String jsonStr = String.valueOf(claims.get("currentUser"));
//5.1 转为 UserVo
UserVo userVo = JSONUtil.toBean(jsonStr, UserVo.class);
//6. 取用户id 然后从redis中获取 该用户所能访问的 , 所有资源列表(包含按钮资源列表 和白名单列表)
//Long userVoId = userVo.getId();
String urlStr = redisTemplate.opsForValue().get(CacheConstant.PUBLIC_ACCESS_URLS + userVo.getId());
//6.1 转为集合
List<String> urls = JSONUtil.toList(urlStr, String.class);
//7. 校验当前要访问的 资源 是否在用户所能访问的所有资源列表中
//7.1 获取当前要访问的资源路径 方法加路径
String target = request.getMethod() + request.getRequestURI();
//7.2 遍历集合和 当前要访问的资源路径 做比较
for (String url : urls) {
if (antPathMatcher.match(url, target)) {
//9. 如果在 将用户信息存入threadLocal 并放行
UserThreadLocal.setSubject(jsonStr);
return true;
}
}
//8. 如果不在 , 则说明没有权限访问该资源 返回403
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_ERROR);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除threadLocal中的数据 防止内存泄漏
UserThreadLocal.removeSubject();
}
}
把拦截器放在配置里面
package com.zzyl.config;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.zzyl.intercept.UserInterceptor;
import com.zzyl.intercept.UserTokenInterceptor;
import com.zzyl.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* webMvc高级配置
*/
@Configuration
@ComponentScan("springfox.documentation.swagger.web")
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor; //注入小程序拦截器
@Autowired
private UserTokenInterceptor userTokenInterceptor;//注入后台拦截器
@Autowired
private SecurityProperties securityProperties; //白名单
//拦截的时候过滤掉swagger相关路径和登录相关接口
private static final String[] EXCLUDE_PATH_PATTERNS = new String[]{"/swagger-ui.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs",
// 登录接口
"/customer/user/login"};
@Override
public void addInterceptors(InterceptorRegistry registry) {
//小程序拦截器
registry.addInterceptor(userInterceptor).excludePathPatterns(EXCLUDE_PATH_PATTERNS).addPathPatterns("/customer/**");
//后台拦截器
List<String> ignoreUrl = securityProperties.getIgnoreUrl();
registry.addInterceptor(userTokenInterceptor).excludePathPatterns(ignoreUrl).addPathPatterns("/**");
}
/**
* 资源路径 映射
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//支持webjars
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
//支持swagger
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
//支持小刀
registry.addResourceHandler("doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
}
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 序列化
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
builder.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
builder.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
// 反序列化
builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
};
}
}