前言
Demo源码地址:https://gitee.com/ruozxby/springsecuritydemo
springSesurity本质是一串过滤器执行链 我们自定义主要分为三个部分 1.配置文件 2登录认证 3权限认证
大概流程 调用自定义登录—> 成功返回token(同时用户数据存入redis) —>后续调用则进入自定义的jwt校验过滤器,成功则把用户数据、权限数据存入SecurityContextHolder的Context中,后续过滤器会通过context中是否有数据判断是否登录成功—>进入权限认证—>权限认证成功进入接口
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请
求。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
1 配置文件
主要配置需要放行的接口,登录认证失败处理器,权限认证失败处理器,还有些其他配置(权限认证配置,密码编辑器等)
配置文件
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解
public class SecurityConfig {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SpringSecurityUserDetailsConfig springSecurityUserDetailsConfig;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPointImpl;
@Autowired
private MyAccessDeniedHandlerImpl accessDeniedHandlerImpl;
public JwtAuthenticationFilter authenticationJwtTokenFilter() {
return new JwtAuthenticationFilter(redisTemplate);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 提供自定义loadUserByUsername
authProvider.setUserDetailsService(springSecurityUserDetailsConfig);
// 指定密码编辑器
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用basic明文验证
//.httpBasic().disable()
// 禁用默认登录页
//.formLogin().disable()
// 禁用默认登出页
//.logout().disable()
// 前后端分离是无状态的,不需要session了,直接禁用。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((authorizeRequests -> {
authorizeRequests
// 允许直接访问授权登录接口
// .requestMatchers(HttpMethod.POST,"/user/login").permitAll()//当前接口无论登没登录都可以访问
.requestMatchers(HttpMethod.POST, "/user/login").anonymous()//当前接口未登录才能访问
//.requestMatchers(HttpMethod.POST, "/user/login").hasAnyAuthority("权限表示符")//当前接口必须要有某个权限才能访问
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();//其他所有接口登录才能访问
}))
//自定义 authenticationJwtTokenFilter过滤器 把用户数据存入 SecurityContextHolder中,方便后面的过滤器判断是否登录
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
// 设置自定义异常处理器
.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPointImpl)) //认证失败处理器
.exceptionHandling(exceptions -> exceptions.accessDeniedHandler(accessDeniedHandlerImpl)) //授权失败处理器
.authenticationProvider(authenticationProvider());
return http.build();
}
}
登录认证失败处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//从本地线程获取errMsg
Object errMsg = ThreadLocalUtil.getLaterRemove("errMsg");
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
ResponseResult result= new ResponseResult(HttpStatus.FORBIDDEN.value(), Objects.isNull(errMsg) ? "登录认证失败,请重新登录" : errMsg);
response.getWriter().print(JSON.toJSONString(result));
}
}
权限认证失败处理器
@Component
public class MyAccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//获取失败信息
//返回
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
ResponseResult result= new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"权限不足"); //accessDeniedException.getMessage();
response.getWriter().print(JSON.toJSONString(result));
}
}
工具类
JWTUtil
public class JWTUtil {
//加密算法
private final static SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
//私钥 / 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取,切记这个秘钥不能外露,只在服务端使用,在任何场景都不应该流露出去。
// 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
private final static String secret = "secretKey";
// 过期时间(单位秒)/ 2小时
private final static Long access_token_expiration = 7200L;
//jwt签发者
private final static String jwt_iss = "jlLiu";
//jwt所有人
private final static String subject = "Liujl";
/**
* 创建jwt
*
* @return 返回生成的jwt token
*/
public static String generateJwtToken(Map<String, Object> claims) {
// 头部 map / Jwt的头部承载,第一部分
// 可不设置 默认格式是{"alg":"HS256"}
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
//载荷 map / Jwt的载荷,第二部分
/* Map<String,Object> claims = new HashMap<String,Object>();
//私有声明 / 自定义数据,根据业务需要添加
claims.put("id","123456");
claims.put("userName", "admin");*/
//标准中注册的声明 (建议但不强制使用)
//一旦写标准声明赋值之后,就会覆盖了那些标准的声明
claims.put("iss", jwt_iss);
/* iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
*/
//下面就是在为payload添加各种标准声明和私有声明了
return Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
.setHeader(map) // 头部信息
.setClaims(claims) // 载荷信息
.setId(UUID.randomUUID().toString()) // 设置jti(JWT ID):是JWT的唯一标识,从而回避重放攻击。
.setIssuedAt(new Date()) // 设置iat: jwt的签发时间
//.setExpiration(new Date(System.currentTimeMillis() + access_token_expiration * 1000)) // 设置exp:jwt过期时间
.setSubject(subject) //设置sub:代表这个jwt所面向的用户,所有人
.signWith(SIGNATURE_ALGORITHM, secret)//设置签名:通过签名算法和秘钥生成签名
.compact(); // 开始压缩为xxxxx.yyyyy.zzzzz 格式的jwt token
}
public static String getUserIdByJwt(String jwt) {
Claims claims = getClaimsFromJwt(jwt);
return Objects.isNull(claims) ? null : Objects.toString(claims.get("userId"));
}
/**
* 从jwt中获取 载荷 信息
*
* @param jwt
* @return
*/
private static Claims getClaimsFromJwt(String jwt) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
}
ThreadLocalUtil
public class ThreadLocalUtil {
private static final ThreadLocal<Map<String, Object>> threadLocal = ThreadLocal.withInitial(() -> new HashMap<>(10));
public static Map<String, Object> getThreadLocal() {
return threadLocal.get();
}
public static Object get(String key) {
Map<String, Object> map = threadLocal.get();
return map.get(key);
}
public static void set(String key, Object value) {
Map<String, Object> map = threadLocal.get();
map.put(key, value);
}
public static void set(Map<String, Object> keyValueMap) {
Map<String, Object> map = threadLocal.get();
map.putAll(keyValueMap);
}
public static void remove() {
threadLocal.remove();
}
public static <T> T remove(String key) {
Map<String, Object> map = threadLocal.get();
return (T) map.remove(key);
}
public static Object getLaterRemove(String key) {
Map<String, Object> map = threadLocal.get();
remove();
return map.get(key);
}
}
2 登录验证
登录验证主要分为两部分
1 自定义用户校验:通过前端传入的账号密码校验用户是否合法
2 自定义jwt过滤器:校验token是否合法,合法则把用户数据、权限数据放入SecurityContextHolder的Context中,后续过滤器链会通过Context中是否有用户数据判断登录校验是否通过
3 登录接口: 在登录接口中调用AuthenticationManager的authenticate方法,最终他会调用我们自定义的用户校验,以及选择的密码加密方式校验账号密码,成功则生成token,存入redis后返回
1 自定义用户校验
实现UserDetailsService接口 ,重写 loadUserByUsername 方法
@Component
public class SpringSecurityUserDetailsConfig implements UserDetailsService {
/**
* 自定义查询用户
*
* @param username
* @return
* @throws
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//从数据库查询用户信息 权限信息
User user = null;
if (username.equals("root")) {
// $2a$10$XDrWFhZC4btSkQQ4hs0Yte0RyZFjtts0wh4swL4Rv11arnyVhJBPy 123456
user = new User(1L, "root", "$2a$10$XDrWFhZC4btSkQQ4hs0Yte0RyZFjtts0wh4swL4Rv11arnyVhJBPy");
}
//有问题 UsernameNotFoundException
if (user == null) {
ThreadLocalUtil.set("errMsg","登录失败,当前账号不存在");
throw new UsernameNotFoundException("");
}
//查询用户的权限信息
LoginUser loginUser = new LoginUser();
//权限list 正常从数据库查询 rbac模型 用户->角色->权限
ArrayList<String> roles = new ArrayList<>(List.of("menu1", "menu1"));
//封装成userDetails返回
loginUser.setUser(user);
loginUser.setPermissions(roles);
return loginUser;
}
}
LoginUser 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
/**
* 自定义权限list
*/
private List<String> permissions;
@JSONField(serialize = false) //不序列化,序列化redis会报错
private List<SimpleGrantedAuthority> authority;
/**
* 权限list 框架从这个接口过去权限list
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//单例模式,减少重复调用
if (Objects.isNull(authority)){
authority = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
return authority;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
user实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
// 序列化 ID,通常是一个唯一的 long 值,用于版本控制
private static final long serialVersionUID = 1L;
private Long id;
private String userName;
private String password;
}
2 自定义jwt过滤器
在过滤器中校验token合法性以及插入 用户数据、权限数据到context中
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public JwtAuthenticationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 自定义filter 用户登录则把用户数据存入SecurityContextHolder上下文中,没有则直接放行,后面过滤器会自行判断是否有数据(是否登录)
*
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
//TODO token时效校验
if (StringUtils.isNotBlank(jwt)) {
//token存在,通过token获取userId,通过userId从redis中拿用户数据
String userId = JWTUtil.getUserIdByJwt(jwt);
LoginUser redisLoginUser = (LoginUser) redisTemplate.opsForValue().get("api:login:" + userId);
Optional.ofNullable(redisLoginUser).ifPresent(loginUser -> {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
});
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
// 从请求头或其他地方获取 JWT
/* String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
return token.substring(7);
}
return null;*/
return request.getHeader("token");
}
}
记得在配置文件中加入自定义的过滤器
//自定义 authenticationJwtTokenFilter过滤器 把用户数据存入 SecurityContextHolder中,方便后面的过滤器判断是否登录
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
3 登录接口
调用springSecurity AuthenticationManager.authenticate 方法进行登录校验 最终会调用到自定义的用户查询逻辑,成功生成token,存入reids后返回
//调用springSecurity AuthenticationManager.authenticate 方法进行登录校验 最终会调用到自定义的用户查询逻辑
Authentication authenticate;
try {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
authenticate = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
ThreadLocalUtil.set("errMsg",Objects.isNull(ThreadLocalUtil.get("errMsg")) ? "登录失败,账号或密码错误" : ThreadLocalUtil.get("errMsg"));
throw new RuntimeException(e);
}
//校验是否认证通过
if (Objects.isNull(authenticate)){
ThreadLocalUtil.set("errMsg","登录失败,账号或密码错误");
throw new RuntimeException("登录失败");
}
//认证通过生产jwt,存入redis后返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
//创建jwt
String jwt = JWTUtil.generateJwtToken(new HashMap<>() {{put("userId", userId);}});
//存入redis
redisTemplate.opsForValue().set("api:login:"+userId, loginUser);
return jwt;
3 权限验证
权限验证分为两个部分
1 配置文件开启权限校验
2 在接口上添加权限校验注解:注解指定该接口所需权限,自定义jwt校验中,成功会把权限数据存入context中,spring会去拿出做比较
1开启权限校验
在配置文件上通过注解开启权限校验
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解
2 在接口上添加权限校验注解
默认权限校验
hasAnyAuthority是spring提供的默认权限校验中的一种
@PreAuthorize("hasAnyAuthority('menu2')")
在UserDetails实现类的实体类中,有个getAuthorities方法,spring会调用该方法
private User user;
/**
* 自定义权限list
*/
private List<String> permissions;
@JSONField(serialize = false) //不序列化,序列化redis会报错
private List<SimpleGrantedAuthority> authority;
/**
* 权限list 框架从这个接口获取权限list
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//单例模式,减少重复调用
if (Objects.isNull(authority)){
authority = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
return authority;
}
其中 hasAnyAuthority 是spring提供的校验方式,我们也可以自定义权限校验
自定义权限校验
自定义权限校验器
@Component("sspc")
public class SpringSecurityPermissionsConfig {
public boolean myHasAnyAuthority(String authority){
//获取用户权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
return permissions.contains(authority);
}
}
接口上
//@PreAuthorize("hasAnyAuthority('menu1')")
@PreAuthorize("@sspc.myHasAnyAuthority('menu1')")
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。