前言 : SpringSecurity是Spring家族中的安全框架,主要功能有两个认证(authentication)授权(authorization)
认证
认证核心过滤器链流程图
- 前端发送请求
- AuthenticationFilter拦截请求 调用UsernamePassowordAuthenticationToken
- UsernamePassowordAuthenticationToken 将前端发送的表单中username和password封装成对象
- AuthenticationMananger调用认证器,认证UsernamePassowordAuthenticationToken 对象
- 认证UsernamePassowordAuthenticationToken 对象的时候,默认情况AuthenticationProvider从内存中读取用户信息。我们可以通过实现UserDetailService接口来实现自定义的获取用户对象的逻辑。
- 当UsernamePassowordAuthenticationToken 对象 与UserDetail对象比较之后,两个相等AuthenticationMananger会生产一个Authentication对象并将UserDetail对象封装成属性通过验证。不相等直接报错没有通过验证
- 所有通过验证的Authentication会被保存到SecurityContextHolder中
如果要自定义获取到存放的用户信息,需要提供一个密码校验器来比较密码
认证代码
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.6.6</version>
</parent>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
<version>5.8.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.26</version>
</dependency>
</dependencies>
当我们引入了spring-boot-starter-security场景的时候,SpringSecurity就已经生效了。当我们没有认证去访问Controller层时Spring Security会帮我们跳转到登录页面。
注意: 这时候我们的用户名和密码都是固定的,我们不希望这样所以我们要自定义Spring Securiyt的执行链
UserDetailService
用来定义获取数据库的用户信息,这样我们的用户不是固定了
public UserDetialServiceImpl implements UserDetailService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 根据前端传输过来的Username从数据库找到对应的用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(queryWrapper);
// 判断是否存在当前用户
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
};
// 获取当前用户的角色
List<String> roleList = userMapper.getRole(user.getId());
log.info(roleList.toString());
user.setRoles(roleList);
// 将用户信息添加到缓存中
redisTemplate.boundValueOps(String.valueOf(user.getId())).set(user);
return new LoginUser(user);
}
}
LoginUser
因为DetailUserService需要返回UserDetail,我们就创建一个类来继承
public class LoginUser implements UserDetails {
private User user;
// 获取当前用户的权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO 进行处理
log.info(user.getRoles().toString());
return user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());
}
// 获取当前用户的密码
@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是映射数据的实体类对象
BCryptPasswordEncoder
因为我们自定义了获取用户信息,所以我们要提供一个密码校验器
SecurityConfig 配置Spring Security的配置类
// Spring Security配置类需要继承WebSecurityConfigurerAdapter
@Component
public class SecurityConfig exends WebSecurityConfigurerAdapter {
// 设置密码校验器
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder () {
return new BCryptPasswordEncoder();
}
}
注意在保存密码的时候需要将密码加密,如果没有加密就在密码前面加上 {noop}
数据库脚本
create table tb_security_user (
id bigint primary key auto_increment,
username varchar(30) not null comment '用户名',
password varchar(30) not null comment '密码',
is_deleted int not null default 0 comment '是否被删除 0 没有 1 有',
create_time datetime default CURRENT_TIMESTAMP(0) comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP(0) comment '修改时间'
)
Contoller
/login路径是SpringSecurity默认的登录路径(可以修改), 当登录成功的时候返回一个有UserId和Username生产的Token并返回给前端
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping
public Result login (@RequestBody User user) {
String token = loginService.login(user);
return Result.succeed().data("token", token);
}
}
Service
public interface LoginService {
String login(User user);
}
ServiceImpl
@Service
public class LoginServiceImpl implement LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public String login(User user) {
// 创建UsernamePasswordAuthenticationToken 将前端传递过来的表单值封装
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// 在Spring Security的配置文件中配置上Authentication 对象
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 判断是否登录成功
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 取出登录成功的LoginUser对象
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
// 生成token
Map<String, Object> payload = new HashMap<>();
payload.put("id", loginUser.getUser().getId());
payload.put("username", loginUser.getUsername());
String token = JWTUtil.createToken(payload, SystemConst.SALT.getBytes(StandardCharsets.UTF_8));
return token;
}
}
SecurityConfig
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
自定义过滤器
因为我们判断一个用户是否已经登录是通过请求头是否携带token所以我们要自定义一个过滤器来优化判断是否登录 TokenFilter
@Component
public class TokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 请求头没有携带token的情况,表示当前用户没有登录
String token = request.getHeader("token");
if (!StringUtils.hasLength(token)) {
// 没有携带token的情况
// 放行
filterChain.doFilter(request, response);
return;
}
// 表示当前用户登录过,但不知道是否登出,我们判断缓存中是否有当前对象
// 解析token
JWT jwt = JWTUtil.parseToken(token);
Long id = ((NumberWithFormat) jwt.getPayload("id")).longValue();
String username = (String) jwt.getPayload("username");
// 根据id在缓存中查找是否存在
User user = (User) redisTemplate.opsForValue().get(String.valueOf(id));
// 是否登出
if (Objects.isNull(user)) {
throw new RuntimeException("登录失败");
}
LoginUser loginUser = new LoginUser(user);
// authorities 权限 DetailUser.get
// 用户认证成把用户信息存放到SecurityContex中
List list = new ArrayList();
list.add(new SimpleGrantedAuthority("test"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null,list);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
配置Spring Security
@Override
protected void configure(HttpSecurity http) throws Exception {
// 基本配置
http
.csrf().disable()
// 不用生产session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 处理认证请求
.authorizeRequests()
.antMatchers("/login").anonymous()
.anyRequest().authenticated();
// 添加过滤器
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
}
logout
标签:return,Spring,token,authentication,user,Override,new,Security,public From: https://blog.51cto.com/u_15497049/7112761只需要根据 userId把缓存移除就可以了 Contoller层的路径为 /logout