认证部分可以看具体笔记

Spring Security过滤链

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。【判断你的用户名和密码是否正确】
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException 。【处理认证授权过程中的所有异常,方便统一处理】
  • FilterSecurityInterceptor:负责权限校验的过滤器。【它会判断你登录成功的用户是“谁”,“你”具有什么权限,当前访问的资源需要什么权限】

过程详解:

当前端提交用户名和密码过来时,进入了UsernamePasswordAuthenticationFilter过滤器。

  • UsernamePasswordAuthenticationFilter过滤器里,将传进来的用户名和密码被封装成了**Authentication对象【这时候最多只有用户名和密码,权限还没有】,Authentication对象通过ProviderManagerauthenticate方法**进行认证。

    • 在**ProviderManager**里面,通过调用DaoAuthenticationProviderauthenticate方法进行认证。

      • DaoAuthenticationProvider里,调用**InMemoryUserDetailsManagerloadUserByUsername方法查询用户。【传入的参数只有用户名字符串**】

        • InMemoryUserDetailsManagerloadUserByUsername方法

          里执行了以下操作

          1. 根据用户名查询对于用户以及这个用户的权限信息【在内存里查
          2. 把对应的用户信息包括权限信息封装成**UserDetails对象**。
          3. 返回**UserDetails对象**。
      • 返回给了DaoAuthenticationProvider,在这个对象里执行了以下操作

        1. 通过**PasswordEncoder对比UserDetails中的密码和Authentication密码是否正确。【校验密码(经过加密的)**】
        2. 如果正确就把**UserDetails权限信息设置到Authentication**对象中。
        3. 返回**Authentication**对象。
  • 又回到了过滤器里面UsernamePasswordAuthenticationFilter

    1. 如果上一步返回了**Authentication**对象

      就使用**SecurityContextHolder.getContext().setAuthentication()**方法存储对象。

      其他过滤器会通过SecurityContextHolder来获取当前用户信息。【当前过滤器认证完了,后面的过滤器还需要获取用户信息,比如授权过滤器】

彩色字体的类均是比较重要的接口,在实现认证的过程中均需要自定义一个类来重新实现或者变更为Spring中其他实现类。

概念速查:

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户的权限等相关信息。
  • AuthenticationManager接口:定义了认证Authentication的方法 ,实现类是ProviderManager
    • 它的实现类是**ProviderManager,它的功能主要是实现认证用户,因为在写登录接口时,可以通过配置类的方式,注入Spring容器中来使用它的authenticate方法**。
  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法
    • 原本的实现类是**InMemoryUserDetailsManager**,它是在内存中查询,因为我们需要自定义改接口。
  • **UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication**对象中。
    • 当我们自定义**UserDetailsService接口时,需要我们定义一个实体类来实现这个接口来供UserDetailsService**接口返回。【注意是实体类】

基于Cookie与Session的方案

  • 优点:实现简单,成熟
  • 缺点:集群情况下实现困难,反向代理配置繁琐,移动端不友好

基于jwt的方案

  • 优点:无状态,服务器资源占用小,天生支持分布式,实现简单
  • 缺点:安全性不高,jwt没办法实现过期将用户踢下线,只能前端实现,但是jwt在有效期内依然有效。

基于自定义Token+Redis

  • 优点:服务器可以实现对用户的下线等操作,支持分布式
  • 缺点:相比jwt是中心化方案,对服务器存储有要求,分布式需要用到Redis

技术路线

  • 可以使用controller,然后不使用formlogin就不会注入UserPasswordAuthenticationFilter(比较灵活,实现拓展功能比较简单)
  • 也可以重写UserPasswordAuthenticationFilter,然后重写它的认证成功,失败接口

鉴权

鉴权发生在认证之后,因此鉴权入口在FilterSecurityInterceptor中,原理不多赘述,有三种实现方法。

  1. 直接重写一个AccessDecisionManager,将它用作默认的AccessDecisionManager,并在里面直接写好鉴权逻辑。
  2. 再比如重写一个投票器,将它放到默认的AccessDecisionManager里面,和之前一样用投票器鉴权。
  3. 我看网上还有些博客直接去做FilterSecurityInterceptor的改动。

用户权限

UserUserDetails 用户权限

GrantedAuthority 一般使用实现类SimpleGrantedAuthority就够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 当更新Admin时,传入的authorities是JSON格式,而getAuthorities()
* 需要Collection<? extends GrantedAuthority> 格式
*
* 自定义JSON反序列化
* CustomAuthorityDeserializer
* @return
*/
@Override
@JsonDeserialize(using = CustomAuthorityDeserializer.class)
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = roles
.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());

return authorities;
}

鉴权规则源

FilterInvocationSecurityMetadataSource 鉴权规则源

用于返回当前API所需的ROLE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 菜单权限控制
* 根据请求url分析请求所需的角色
* 访问该url资源所需角色
*/
@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {

@Autowired
private IMenuService menuService;

AntPathMatcher antPathMatcher = new AntPathMatcher();

/**
* 根据request请求获取访问资源所需权限
* 用户GrantedAuthority与ConfigAttribute一对比就知道用户有没有权限访问该api了
* @param o
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取请求的url
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> menus = menuService.getMenusWithRole();
for (Menu menu : menus) {
//判断请求url与菜单角色是否匹配
if (antPathMatcher.match(menu.getUrl(),requestUrl)) {
//得到rname,并将其放入一个string集合中,一个url可能对应多个角色
String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
//将得到的角色放入List里
return SecurityConfig.createList(str);
}
}
//给一个默认登录即可访问角色
return SecurityConfig.createList("ROLE_LOGIN");
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return false;
}
}

授权管理

AccessDecisionManager

用户GrantedAuthority与ConfigAttribute一对比就知道用户有没有权限访问该api了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Authentication authentication
* 认证之后的身份
*
* Object o
* Object对象这里是要访问的受保护资源,他是一个FilterInvocation类型,你可以通过这个对象获取当前所访问的路径
*
* Collection<ConfigAttribute> configAttributes
* Collection集合就是要操作当前资源,所需要的角色。
*
* https://blog.csdn.net/weixin_43836204/article/details/106310730
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute attribute : configAttributes) {
//当前url资源所需角色
String needRole = attribute.getAttribute();
//判断用户角色是否为url所需角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
//判断角色是否是登录即可访问的角色,此角色在CustomFilter中设置
if ("ROLE_LOGIN".equals(needRole)) {
//判断是否登录
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录");
} else {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员");
}

授权错误处理器

AccessDeniedHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @program: cowork-back
* @description: 当访问接口没权限时,自定义返回结果
* @author: disda
* @create: 2022-01-24 15:35
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
PrintWriter out = httpServletResponse.getWriter();
RespBean bean = RespBean.error("权限不足,请联系管理员!");
bean.setCode(403);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}

配置授权过滤器

OncePerRequestFilter

  • jwt版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.disda.cowork.config.security.components;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @program: cowork-back
* @description: jwt 登录授权过滤器
* @author: disda
* @create: 2022-01-24 14:52
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Value("${jwt.tokenHeader}")
private String tokenHeader;

@Value("${jwt.tokenHead}")
private String tokenHead;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

/**
* 浏览器传过来的request请求中
* key value
* key为配置中的tokenHeader
* value为配置中的tokenHead+空格+token
*/

String authHeader = request.getHeader(tokenHeader);
// 存在token
if(authHeader!=null&&authHeader.startsWith(tokenHead)){
String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
// token存在用户名,但是未登录
if(username!=null && SecurityContextHolder.getContext().getAuthentication() == null){
// 登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证Token是否有效,重新设置用户对象
if(jwtTokenUtil.validateToken(authToken,userDetails)){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request,response);
}
}

配置Spring Security

SecurityConfig extends WebSecurityConfigurerAdapter

  • jwt版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用JWT,不需要csrf,关闭csrf防范
http
.csrf().disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//授权认证
.authorizeRequests()
//所有请求都要求认证,必须登录后被访问
.anyRequest().authenticated()
//动态权限配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilter);
return object;
}
})
.and()
//禁用缓存
.headers()
.cacheControl();

//添加jwt 登录授权过滤器
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

//异常处理
// 添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);

}