提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
课程来自: https://www.bilibili.com/video/BV1mm4y1X7Hc?p=1&vd_source=345a382f2c86d3441cc342a80fc25545
一、认证
1.登录校验流程
2.原理初探
2.1 认证的简单流程
Springsecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
- UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登陆请求。(入门案例的认证工作主要由它负责处理)
- ExceptionTransationFilter:负责处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException异常。
- FilterSecurityInterceptor:负责处理权限校验和过滤器。
我们可以通过Debug查看当前系统中Springsecurity过滤器链中有哪些过滤器及它们的顺序。
2.2 认证的详细流程
- Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。
- AuthenticationManager接口:定义了认证Authentication的方法。
- UserDetailsService接口:加载用户特定数据的核心接口;其中定义了一个根据用户名查询用户信息的方法(loadUserByUsername(String username))。
- UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回;然后将这些信息封装到Authentication对象中。
2.3 思路分析
2.4 解决问题(登录)
2.4.1 自定义登录接口
- 我们需要自定义登录接口,让springsecurity对这个接口放行。(不需要登录也能访问)
- 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
- 认证成功后,需要生成一个jwt(token)返回给前端。
- 为了让用户下次请求时能通过jwt(token)识别出具体的是哪个用户,需要把用户信息存入redis(可以把用户id作为key)。
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Override
public Map login(User user) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败!!!");
}
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
HashMap<String, Object> map = new HashMap<>();
Long userid = loginUser.getUser().getId();
map.put("userid",userid);
String token = JWTUtil.createToken(map, "beacon_key".getBytes());
String redis_key = "login:"+userid;
redisTemplate.opsForValue().set(redis_key, JSONUtil.toJsonStr(loginUser));
HashMap<String, Object> maps = new HashMap<>();
maps.put("token",token);
return maps;
}
}
2.4.2 自定义UserDetailsService(在这个实现列中去查询数据库)
@Component
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误!");
}
return new LoginUser(user);
}
}
2.5 解决问题(校验)
2.5.1 自定义Jwt认证过滤器
-
获取token -
解析token获取其中的userid -
通过userid从redis中获取用户信息 -
把用户信息存入到SecurityContextHolder(获取权限信息封装到authentication中)为了让后面的filter知道这个请求是已认证的 -
放行过滤器 @Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (Objects.isNull(token)) {
filterChain.doFilter(request, response);
return;
}
String userid;
try {
JWT jwt = JWTUtil.parseToken(token);
JWTPayload payload = jwt.getPayload();
userid = payload.getClaimsJson().get("userid").toString();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法!!!");
}
String reids_key = "login:" + userid;
String loginUserStr = (String) redisTemplate.opsForValue().get(reids_key);
LoginUser loginUser = JSONUtil.toBean(loginUserStr, LoginUser.class);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录!!!");
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
2.5.2 配置自定义jwt过滤器到过滤器链中
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.antMatchers(securityProperty.getOpenApi()).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
2.6 密码加密存储
- 实际项目中我们不会把密码明文存储在数据库中。
- SpringSecurity默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password ;它会根据id去判断密码的加密方式。(我们一般不会采用这种方式,因此需要替换PasswordEncoder)
- 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
- 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
- 我们可以定义一个Springsecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2.6 解决问题(退出登录)
我们只需要定义一个接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的用户数据即可。|
@Override
public Results logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
redisUtils.del("login:"+userid);
return Results.success("注销成功!!!");
}
二、授权
1. 权限系统作用
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
2. 授权基本流程
- 在SpringSecurity中,会使用默认的FilterSecuritylnterceptor来进行权限校验。
- 在FilterSecuritylnterceptor中会从SecurityContextHolder中获取Authentication用户信息(获取其中的权限信息),对比当前用户是否拥有访问当前资源所需的权限。
- 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication中;然后设置我们的资源所需要的权限即可。
3. 授权的实现
Springsecurity为我们提供了基于注解的权限控制方案(这也是我们项目中主要采用的方式)。 我们可以使用注解去指定访问对应的资源所需的权限。
- 第一步需要先开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
- 可以使用对应的注解。(@PreAuthorize(“hasAuthority(‘test’)”))
@GetMapping("user/ceshi")
@PreAuthorize("hasAuthority('test')")
public Results ceshi(){
return Results.success(null,"测试成功!!!");
}
4.从数据库查询权限信息
4.1 RBAC(Role Based Access Control)
基于角色的权限控制;这是目前最常被开发者使用也是相对易用、通用权限模型。
4.2 代码实现
省略…
5. 自定义失败处理
目的:如果在认证失败或者是授权失败的情况下也能和接口一样返回相同结构的json,可以让前端能对响应进行统一的处理。
处理机制:
- 要实现这个功能我们需要知道SpringSecurity的异常处理机制;
- (在Springsecurity中) 如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到;
- (在Springsecurity中)在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
做法:
- 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint接口对象的方法去进行异常处理。
- 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler接口对象的方法去进行异常处理。
- 因此,我们需要自定义异常处理,自定义类实现AuthenticationEntryPoint接口和AccessDeniedHandler接口的方法,然后配置给SpringSecurity即可。
5.1 认证失败处理
-
自定义处理器 @Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Results results = Results.fail(HttpStatus.UNAUTHORIZED.value(), null, "用户认证失败,请重新登录");
String json = JSONUtil.toJsonStr(results);
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(json);
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
在SpringSecurityConfig配置文件中配置
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
5.2 授权失败处理
-
自定义处理器 @Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Results results = Results.fail(HttpStatus.FORBIDDEN.value(), null, "权限不足");
String jsonStr = JSONUtil.toJsonStr(results);
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(jsonStr);
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
在SpringSecurityConfig配置文件中配置
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
6. 跨域
浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求(默认情况下是被禁止的)。 同源策略 :要求源相同才能正常进行通信(即协议、域名、端口号都完全一致)
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。 所以我们就要处理一下,让前端能进行跨域请求。
1. 先对SpringBoot配置,允许跨域请求
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET","POST","DELETE","PUT")
.allowedHeaders("*")
.maxAge(3600);
}
}
2.开启Springsecurity的跨域访问
由于我们的资源都会受到Springsecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。
http.csrf().disable();
7. 遗留的小问题
7.1 其他权限校验方法
之前是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。 hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority方法,该方法内部是调用authentication的getAuthorities方法获取用户的权限列表,然后判断我们访问的方法参数的权限是否在权限列表中。
Springsecurity还为我们提供了其它方法例如:
- hasAnyAuthority:
hasAnyAuthority方法可以传入多个权限,只要用户有其中任意一个权限都可以访问对应资源。 - hasRole:
hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。 - hasAnyRole:
hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。
7.2 自定义权限校验方法
@Component("sywl")
public class SywlExpressionRoot {
public boolean hasAuth(String authority) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
return permissions.contains(authority);
}
}
在SPEL表达式中使用@sywl相当于获取容器中bean的名字为sywl的对象。然后再调用这个对象的hasAuth方法。
@GetMapping("user/ceshi")
@PreAuthorize("@sywl.hasAuth('test')")
public Results ceshi(){
return Results.success(null,"测试成功!!!");
}
7.3 基于配置的权限控制
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.antMatchers(securityProperty.getOpenApi()).permitAll()
.anyRequest().authenticated()
.antMatchers("/user").hasAuthority("user:get");
}
7.4 csrf
https://blog.csdn.net/freeking101/article/details/86537087
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
Springsecurity去防止CSRF攻击的方式就是通过csrf_token。 后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。 我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。 但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
7.5 登录成功的处理器
实际上是在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调AuthenticationSuccessHandler的方法进行认证成功后的处理的。 AuthenticationSuccessHandler就是登录成功处理器;我们也可以自己去自定义成功处理器进行成功后的相应处理。
7.6 注销成功的处理器
实际上在UsernamePasswordAuthenticationFiter进行登录认证的时候,如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。 AuthenticationFailureHandler就是登录失败处理器;我们也可以自己去自定义失败处理器进行失败后的相应处理。
7.7 其他认证方案畅想
https://www.bilibili.com/video/BV1mm4y1X7Hc?p=40&vd_source=345a382f2c86d3441cc342a80fc25545
|