目录
前言
今日进度
详细过程
相关知识点
前言
昨天重构了数据库并实现了登录功能,今天继续进行开发,创作不易,请多多支持~
今日进度
添加过滤器、实现登出功能、实现用户授权功能校验
详细过程
一、添加过滤器
自定义过滤器作用:自定义的 JWT 认证过滤器,用于解析请求头中的 token
,验证用户身份,并将用户信息存入 SecurityContextHolder
,从而支持 Spring Security 的认证和授权功能。
package com.example.familyeducation.utils.filter;
import com.example.familyeducation.entity.LoginUser;
import com.example.familyeducation.utils.JwtUtil;
import com.example.familyeducation.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import static com.example.familyeducation.utils.constants.RedisConstants.LOGIN_USER_KEY;
/**
* @ClassDescription:自定义过滤器,获取请求头中的token并解析
* @Author:小菜
* @Create:2024/11/6 10:34
**/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.获取请求头中的token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//2.token为空,直接放行
filterChain.doFilter(request, response);
return;
}
//3.token不为空
//3.1解析token中的id
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
//TODO 使用统一异常类封装
throw new RuntimeException("token非法");
}
//4.根据id从redis中获取用户信息
String key = LOGIN_USER_KEY + id;
LoginUser loginUser = redisCache.getCacheObject(key);
if(Objects.isNull(loginUser)){
throw new RuntimeException("redis中数据为空,用户未登录");
}
//5.将用户信息存入SecurityContextHolder
//TODO获取当前用户权限信息封装到Authentication 直接从LoginUser中获取即可
//5.1封装用户信息到Authentication
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//5.2将信息存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//6.放行
filterChain.doFilter(request,response);
}
}
添加完过滤器后记得去将过滤器添加上
//添加自定义过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
这样访问其他接口时,就会自动解析token并进行对应操作
二、登出功能
在登出功能中我们将数据从Redis中进行删除,那样其他接口就无法访问到这些数据,在过滤器中就会显示用户为登录,实现登出功能
@Override
public ResponseResult logout() {
//1.从SecurityContextHolder中查找到Authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//2.从Authentication中获取LoginUser
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//3.获取id并从Redis中删除,那样下次再进行过滤时就查询不到Redis中的数据,显示用户未登录
Integer userId = loginUser.getUser().getId();
redisCache.deleteObject(LOGIN_USER_KEY+userId);
return new ResponseResult(200,"成功退出登录");
}
三、权限校验
权限校验今天踩了很多坑,因为之前没有接触过这个
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
首先,权限校验的思路如下:
- 请求先到过滤器,此时的请求中携带token
- 在过滤器中解析token得到id并从Redis中取得数据(用户数据和权限信息)
- 过滤器将数据存到SecurityContextHolder,这样请求进行过程中就能得到数据,可以来判断是否有权限,若无权限则返回403
首先我们先去配置中配置一下
我们要开启一下配置并指定路径与权限的关系,指定哪些路径需要哪些权限才能访问
package com.example.familyeducation.config;
import com.example.familyeducation.utils.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
.antMatchers("/hello/**").hasRole("ADMIN")//对于/hello的路径,只有ADMIN权限的用户才能访问
.antMatchers("/ok/**").hasAnyRole("ADMIN","USER")//对于/ok的路径,ADMIN和USER权限的用户都可以访问
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//添加自定义过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
配置完后,因为登录成功后才能得到用户数据和权限,所以我们要去UserDetailsServiceImpl中完成之前的TODO将用户权限信息添加到Login中
package com.example.familyeducation.service.impl;
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.familyeducation.entity.LoginUser;
import com.example.familyeducation.entity.User;
import com.example.familyeducation.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @ClassDescription:
* @Author:小菜
* @Create:2024/11/4 19:06
**/
//这里继承的是security中的一个默认接口,重写其中的查询用户方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername,username);
User user = userMapper.selectOne(wrapper);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//TODO根据用户查询权限信息 添加到LoginUser中
//上面的user信息中已经包含了权限信息,但是我们还是要单独把权限提出来
List<String> list = new ArrayList<>();//这里list就是LoginUser中的redisAuthorities
String role = user.getRole();
if(role.equals("admin")){
list.add("ROLE_ADMIN");//注意这里添加自定义权限时要加前缀ROLE_,SpringSecurity会默认根据ROLE_去查找权限
} else if (role.equals("teacher")) {
list.add("ROLE_TEACHER");
}
//封装成UserDetails对象返回
return new LoginUser(user,list);
}
}
同时在LoginUser中我们要进行authorities的管理并编写getAuthorities()方法保证过滤器能获取到用户权限
package com.example.familyeducation.entity;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> redisAuthorities;
public LoginUser(User user, List<String> redisAuthorities) {
this.user=user;
this.redisAuthorities=redisAuthorities;
}
@JSONField(serialize = false)//保证该集合不被序列化
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}else{
authorities = redisAuthorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
}
@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;
}
}
最后我们使用Apifox进行测试
先登录一下,成功返回了token,同时Redis中保存了用户信息和权限
好,接下来对权限进行测试,hello路径是只有ADMIN能进行访问,而我们当前用户刚好是管理员,所以得到了返回信息,测试成功!
相关知识点
登录功能思路:
- 请求先到过滤器,过滤器检查到token为空,直接放行
- 放行后请求到controller层,并调用service层的实现类LoginServiceImpl
- Service实现类LoginServiceImpl中会对认证信息进行验证,于是调用SpringSecurity中的authenticationManager.authenticate()方法进行检验
- 这个方法会自动调用UserDetailsService中的loadUserByUsername进行用户信息验证
- 在UserDetailsService中进行用户数据查询,同时将用户信息中的权限进行封装
- 最后将用户信息和权限信息封装成LoginUser进行返回到登录实现类LoginServiceImpl中
- 实现类LoginServiceImpl根据返回的信息生成token返回前端,并将loginUser存到Redis
实现授权功能思路:
- 请求先到过滤器,此时的请求中携带token
- 在过滤器中解析token得到id并从Redis中取得数据(用户数据和权限信息)
- 过滤器将数据存到SecurityContextHolder,这样请求进行过程中就能得到数据,可以来判断是否有权限,若无权限则返回403
这里有两个坑,一个是权限能获取到,但是保存到Redis中一直是null,另一个是成功保存了权限,但是测试时一直显示403。
我们先解决第一个问题,在封装信息时我们会调用LoginUser中的一个获取权限的方法,这里一开始是定义了一个authorities并直接返回,但是就会有一个问题,当一个新方法调用时,authorities会被重新刷新为null,就导致权限信息一直是null,解决方法是定义一个新的List,并将其保存到Redis中,那样调用新方法,我们在过滤器中获取Redis中的权限,就不会是null了。
private User user;
private List<String> redisAuthorities;
public LoginUser(User user, List<String> redisAuthorities) {
this.user=user;
this.redisAuthorities=redisAuthorities;
}
@JSONField(serialize = false)//保证该集合不被序列化
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}else{
authorities = redisAuthorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
}
第二个问题,这个真的给我整无语了,Spring Security 会通过 GrantedAuthority
来验证用户是否拥有特定的权限。例子中,用户访问 /ok/**
时,Spring Security 会检查 SecurityContextHolder.getContext().getAuthentication().getAuthorities()
中是否包含 ROLE_ADMIN
或 ROLE_USER
。
- Spring Security 的
hasRole()
方法默认会自动在角色前加上ROLE_
前缀,所以当你配置.hasRole("ADMIN")
时,实际上是在检查用户是否有ROLE_ADMIN
权限。 - 如果用户的权限中有
ROLE_ADMIN
或ROLE_USER
,则通过访问检查,允许访问这个路径,否则拒绝访问。
所以就是在手动封装权限的时候没有加上前缀导致权限信息一直对不上,所以就显示403了。。。
List<String> list = new ArrayList<>();//这里list就是LoginUser中的redisAuthorities
String role = user.getRole();
if(role.equals("admin")){
list.add("ROLE_ADMIN");//注意这里添加自定义权限时要加前缀ROLE_,SpringSecurity会默认根据ROLE_去查找权限
} else if (role.equals("teacher")) {
list.add("ROLE_TEACHER");
}
ok,大概就是这样,如果有帮到你的话,请多多支持哦!你的鼓励就是我最大的动力,我们下篇再见~