0
点赞
收藏
分享

微信扫一扫

Spring Security - authentication

前言 : SpringSecurity是Spring家族中的安全框架,主要功能有两个认证(authentication)授权(authorization)

认证

认证核心过滤器链流程图

image.png

  1. 前端发送请求
  2. AuthenticationFilter拦截请求 调用UsernamePassowordAuthenticationToken
  3. UsernamePassowordAuthenticationToken 将前端发送的表单中username和password封装成对象
  4. AuthenticationMananger调用认证器,认证UsernamePassowordAuthenticationToken 对象
  5. 认证UsernamePassowordAuthenticationToken 对象的时候,默认情况AuthenticationProvider从内存中读取用户信息。我们可以通过实现UserDetailService接口来实现自定义的获取用户对象的逻辑。
  6. 当UsernamePassowordAuthenticationToken 对象 与UserDetail对象比较之后,两个相等AuthenticationMananger会生产一个Authentication对象并将UserDetail对象封装成属性通过验证。不相等直接报错没有通过验证
  7. 所有通过验证的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

只需要根据 userId把缓存移除就可以了 Contoller层的路径为 /logout

举报

相关推荐

0 条评论