前言
因为这个Spring Security学习的过程比较曲折,最初以为比较简单,但是实际上也确实比较简单,最大的坑点在于,大多数找到的关于Spring Security都不是基于前后端分离进行的配置,解决了一个bug发现了更多的bug,烦不胜烦,直到在著名的学习网站B站看到了这个视频,老师讲的真的简单,比之前看过的尚**、黑*的视频好很多(基于前后端分离),那两个机构的视频看的简直无语,一个安全框架中恨不得给你把Spring boot、mysql、Spring cloud都讲一遍,看得着实费解。如果你需要快速地投入到项目中,这都总结好了,直接用就是了!(版本:boot-2.6.3)
这里的token使用的是使用JWT生成的,这方面的文章比较多,就不赘述了。
Part.1 引入Security
因为是Spring亲儿子的关系,引入十分方便,直接在pom.xml中加入依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Part.2 登录校验接入数据库
Spring security中存在众多过滤器(Filter),登录模块会调用UsernamePasswordAuthentiocationFilter过滤器(重要!)去处理登录请求,只需要写一个类继承UserDetailsService并且实现loadUserByUsername方法就可以进行登陆校验,如果查询到用户并且返回UserDetails即为通过校验。
@Service
@RequiredArgsConstructor
public class SysUserDetailService implements UserDetailsService {
private final SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 从数据库中取出用户信息
SysUser user = sysUserMapper.selectByAccount(account);
System.out.println(user);
// 判断用户是否存在
if(user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
// todo 添加权限
// 返回UserDetails实现类
// 此User为 org.springframework.security.core.userdetails 包下的对象
return new User(user.getAccount(), user.getPassword(), authorities);
}
}
到此还没有完成,如果直接运行会报错:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
Spring security不允许密码是明文,要么标明加密模式,要么自定义密码解码方法。
自定义密码解码方法需要写在Spring security配置类中:
@Configuration
//加载了WebSecurityConfiguration配置类, 配置安全认证策略。
//加载了AuthenticationConfiguration,
@EnableWebSecurity
//用来构建一个全局的AuthenticationManagerBuilder的标志注解
//开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
//Web Security 配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 添加密码校验
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(sysUserDetailService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
System.out.println(rawPassword.toString());
//todo 密码解密
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
System.out.println(rawPassword.toString());
return encodedPassword.equals(rawPassword.toString());
}
});
}
}
Part.3 前后端分离进行校验
首先,需要一个登陆方法
public class LoginServiceImpl {
private final AuthenticationManager authenticationManager;
/**
* 登录获取token
* @param sysUser
* @return token
*/
public String login(SysUser sysUser) {
//封装校验对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getAccount(),sysUser.getPassword());
//使用Manager调用 SysUserDetailService
Authentication authentication = authenticationManager.authenticate(authenticationToken);
//认证失败,内容为空
if (Objects.isNull(authentication)){
throw new RuntimeException("登录失败");
}
return JWTUtil.createToken(String.valueOf(sysUser.getId()),"");
}
}
authenticationManager.authenticate(authenticationToken)可以调起UsernamePasswordAuthentiocationFilter过滤器,随后进入UserDetailsService的实现类中进行登录校验。
登录方法中生成了一个token返回给了前端,如何将token放在http请求的header中并且使用security校验,需要自己建立一个过滤器。
/**
* Token拦截器
* 使用Spring提供的OncePerRequestFilter保证单次
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader(JWTUtil.TOKEN_HEADER);
if (!StringUtils.hasText(token)){
//没有token 放行 后续过滤器会进行校验
filterChain.doFilter(request,response);
return;
}
//解析token
//获取用户信息
//存入SecurityContextHolder 用于全局获取当前用户
SysUser user = new SysUser();
user.setAccount("zhangsan");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
将过滤器注册到SecurityConfig配置文件中。
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//设置权限定义哪些URL需要被保护、哪些不需要被保护。HttpSecurity对象的方法
.authorizeRequests()
.antMatchers("/auth/login").permitAll() //放行login
//认证通过后任何请求都可访问。AbstractRequestMatcherRegistry的方法
.anyRequest().authenticated()
//连接HttpSecurity其他配置方法
.and()
//生成默认登录页,HttpSecurity对象的方法
.formLogin()
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf().disable() //关闭跨站请求伪造
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不通过Session获取SecurityContext
.and().cors() //允许跨域
//.and().exceptionHandling()
//.authenticationEntryPoint(getAccessDeniedHandler()) //无权限默认返回设置
;
//添加Token过滤器 放在UsernamePasswordAuthenticationFilter执行之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
添加过滤器只有一行http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
,上面的是一些关于Security的配置,主要在于.antMatchers("/auth/login").permitAll()
放行请求接口,否则访问登录接口也需要进行token校验,那就生成了一个死循环。
Part4.登出
/**
* 登出系统
*/
public Result logout() {
//1、获取SecurityContextHolder中的对象
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
SysUser sysUser = (SysUser) authentication.getPrincipal();
sysUser.getId();
//2、在存放用户token信息的位置清除用户信息,使其不能在JwtAuthenticationTokenFilter中认证成功,即为登出 一般redis
return Result.success();
}
Part5.1.获取当前登录用户
在JwtAuthenticationTokenFilter中往SecurityContextHolder注入了一个用户对象,在实际的方法中,从中取出即可。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SysUser sysUser = (SysUser) authentication.getPrincipal();
Part5.2.美化认证失败
在SecurityConfig中加入处理器,在Part3的配置文件中已存在。
.and().exceptionHandling() .authenticationEntryPoint(getAccessDeniedHandler()) //无权限默认返回设置
具体的过滤器写法:
/**
* 自定义Security校验失败返回类
*/
@Slf4j
public class AccessDeniedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding(DEFAULT_CHARSET);
response.setContentType(DEFAULT_CONTENT_TYPE);
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(Result.result(ResultCode.ErrTokenUnValid)));
out.flush();
out.close();
}
}
使用**@Bean注入getAccessDeniedHandler()**方法中。