SpringSecurity(一): Springboot 3.2.1 整合 SpringSecurity 完成认证
环境说明:
SpringBoot 3.2.1 、SpringSecurity 6.1.2
认证:校验用户输入的账号信息,通过后,为其颁发“业务token”和“安全token”
业务token作用:传输用户编号,并根据编号换取“安全token”
步骤
1.导包
<!--springsecurity核心包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--用签发业务token包-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!--将java对象在对象和json格式之间转换-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.39</version>
</dependency>
2.自定义扩展用户信息校验规则
目的:将springsecurity默认的登录逻辑迁移到自定义的逻辑上(访问数据库)
/*
实现 UserDetailsService 接口,自定义校验逻辑
@Component让spring知晓自定义的内容
*/
@Component
public class SecurityUserDetailServiceImpl implements UserDetailsService {
@Autowired
private ShopUserService shopUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据用户名查询用户信息
QueryWrapper<ShopUser> qw = new QueryWrapper<>();
qw.eq("user_name",username);
ShopUser shopUser = shopUserService.getOne(qw);
//2.TODO 查询该用户的权限列表并整合对象
return new SecurityUserDetailsImpl(shopUser);
}
}
/*
因为Security内部的loadUserByUsername方法需要的返回值必须是UserDetails类型,项目本身提供的用户类型不适用
按照java多态标准,此类符合UserDetails规范,可以用来返回
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SecurityUserDetailsImpl implements UserDetails {
private ShopUser shopUser;
/*
获取校验用户的权限列表
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/*
获取系统用户的密码信息,用于security后续的密码对比
*/
@Override
public String getPassword() {
return shopUser.getPassword();
}
/*
获取系统用户的用户名信息
*/
@Override
public String getUsername() {
return shopUser.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;
}
}
3.配置security框架
需要配置以下信息:
1.密码解析器
2.认证管理器(提供了用户信息校验逻辑)
3.对security过滤器链定义
@Configuration //标记此类为一个springboot的配置类
@EnableWebSecurity //开启security基于web开发的安全机制
public class SecurityConfig {
@Autowired
private SecurityUserDetailServiceImpl securityUserDetailService;
@Autowired
private SecurityTokenFilter securityTokenFilter;
/*
密码加密器,
用户表中的用户密码等敏感信息都需要加密存储
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//2.配置认证管理器,security框架默认不提供
@Bean
public AuthenticationManager authenticationManager(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//设置securityUserDetailService,告知security框架,按照指定的类进行身份校验
daoAuthenticationProvider.setUserDetailsService(securityUserDetailService);
ProviderManager pm = new ProviderManager(daoAuthenticationProvider);
return pm;
}
//3.配置springsecurity的放行路径等信息
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
//对所有请求按照以下约定进行拦截和放行
http.authorizeHttpRequests(
//requestMatchers 指定匹配路径
//permitAll 让security跳过之前通过requestMatchers匹配到的路径,
auth -> auth.requestMatchers("/shopUser/login").permitAll()
//anyRequest 指定除requestMatchers匹配路径之外的其他路径
//authenticated 让anyRequest匹配到的所有路径都通过security校验
.anyRequest().authenticated()
);
//关闭 防止客户端的 csrf(跨站伪造) 攻击行为 的能力
// 从security过滤器链中撤出 CsrfFilter
http.csrf(csrf -> csrf.disable());
//将自定义的token认证过滤器加入到security-filterChian中,并指定其位置
http.addFilterBefore(securityTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
4.开放自定义的登录接口
目的:不再使用springsecurity提供的默认登陆页面,完全自定义登录逻辑
@RestController
@RequestMapping("shopUser")
public class ShopUserController{
@Resource
private ShopUserService shopUserService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JWTUtil jwtUtil;
@Autowired
private SecurityInfoService securityInfoService;
/**
* 因为 springsecurity 框架提供的登录页面不适用于当前项目(前后端分离)
* @param loginPo
* @return
*/
@RequestMapping(path = "login",method = RequestMethod.POST)
public BaseResult login(@RequestBody LoginPo loginPo){
//1.调用security认证方法
//1.1 封装 token 对象
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginPo.getUserName(),loginPo.getPassword());
//1.2调用管理器的方法
//通过认证的用户将会获得 Authentication ,其中存放用户信息
Authentication authentication = authenticationManager.authenticate(token);
//1.3判断结果
// authentication 如果为null,则表示认证失败
if(Objects.nonNull(authentication)){
SecurityUserDetailsImpl securityUserDetails = (SecurityUserDetailsImpl)authentication.getPrincipal();
//将security颁发的后端凭证存入数据库,此处因为暂时不涉及redis,所以json化后存入数据库
ShopUser shopUser = securityUserDetails.getShopUser();
securityInfoService.save(new SecurityInfo(shopUser.getId(), JSON.toJSONString(authentication)));
//认证通过,办法业务token
HashMap<String, String> serviceToken = jwtUtil.cerateToken(shopUser);
return BaseResult.ok(serviceToken);
}
return BaseResult.error("认证失败");
}
}
5.对其他请求的控制
思想:通过业务token中携带的用户id,换取安全token
@Component
public class SecurityTokenFilter extends OncePerRequestFilter {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private SecurityInfoService securityInfoService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
Integer id = null;
if(token != null && !token.equals("")){
//校验token合法性
try{
//从 otken 中获取指定的 payload
DecodedJWT decodedJWT = jwtUtil.getToken(token);
Claim jwtClaim = decodedJWT.getClaim("id");
id = jwtClaim.asInt();
}catch (Exception e){
System.out.println("token 非法!");
return;
}
//去数据库查询当前用户认证时生成的token
QueryWrapper<SecurityInfo> qw = new QueryWrapper();
qw.eq(SecurityInfo.COL_UID,id);
SecurityInfo securityInfo = securityInfoService.getOne(qw);
String authenticationJson = securityInfo.getAuthenticationJson();
//将存入数据库的安全token转会后端识别的对象
Authentication authentication = JSONObject.parseObject(authenticationJson, Authentication.class);
//将它放入security全局上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
//放行,请求会进入下一个过滤器
filterChain.doFilter(request,response);
}else{
filterChain.doFilter(request,response);
}
}
}