谷粒学院SpringSecurity认证流程详解
创始人
2025-05-30 22:10:39

登录功能前端分析

前端会调用此接口去实现登录

// 登录
export function login(username, password) {return request({url: '/admin/acl/login',method: 'post',data: {username,password}})}

request请求会经过request拦截器,给请求携带一个token的字段,token值是从Cookie中获取的,由于现在是登录阶段,所以没有该值。

===========================config/dev.env.js===========================================
module.exports = merge(prodEnv, {NODE_ENV: '"development"',BASE_API: '"http://localhost:8222"',
})
==========================request.js=====================================
// 创建axios实例
const service = axios.create({baseURL: process.env.BASE_API, // api 的 base_urltimeout: 20000 // 请求超时时间
})// request拦截器
service.interceptors.request.use(config => {if (store.getters.token) {// 让每个请求携带自定义tokenconfig.headers['token'] = getToken()}return config},error => {console.log(error)Promise.reject(error)}
)
==================auth.js=================================
const TokenKey = 'Admin-Token'export function getToken() {return Cookies.get(TokenKey)
}export function setToken(token) {return Cookies.set(TokenKey, token)
}export function removeToken() {return Cookies.remove(TokenKey)
}

没登录时浏览器存储的cookie值

以下是我成功登录的截图

最终前端的请求路径为 http://localhost:8222/admin/acl/login

登录后端分析

1、网关

包含acl的请求路径会被网关拦截,然后去注册中心找对应的服务名,再转发到对应的权限管理模块中去

spring:application:name: service-gatewaycloud:#nacos服务地址nacos:discovery:server-addr: 127.0.0.1:8848#使用服务发现路由gateway:discovery:locator:enabled: trueroutes:- id: service-acluri: lb://service-aclpredicates:- Path=/*/acl/**

2、SpringSecurity

以下是SpringSecurity的用户认证流程,下面我会详细分析

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

  • AuthenticationManager接口:定义了认证Authentication的方法

  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

2.1用户认证核心组件

我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念:当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的,这个不难理解,要是当前登录用户都不能确认了,那A下了一个订单,下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。

我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取Authentication,SecurityContext就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程中传递同一个对象!

现在我们已经知道了Spring Security中三个核心组件:

  1. Authentication:存储了认证信息,代表当前登录用户

  1. SeucirtyContext:上下文对象,用来获取Authentication

  1. SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

Authentication中是什么信息?

  1. Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

  1. Credentials:用户凭证,一般是密码

  1. Authorities:用户权限

2.2 UsernamePasswordAuthenticationFilter

用户在提交完用户名和密码后,请求会首先来到 UsernamePasswordAuthenticationFilter,执行其 doFilter方法。

这个方法 UsernamePasswordAuthenticationFilter 并没有实现,而是其父类AbstractAuthenticationProcessingFilter 实现的。

流程如下:

  1. 判断是否是登陆的请求,不是的话直接放过

  1. 调用子类的attemptAuthentication进行认证

  1. 认证成功执行successfulAuthentication方法,失败则返回

  1. 根据用户名生成token

  1. 将权限信息存入redis

  1. 给前端返回token

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;// 判断是否是需要验证方法(是否是登陆的请求),不是的话直接放过if (!requiresAuthentication(request, response)) {chain.doFilter(request, response);return;}// 登陆的请求开始进行验证Authentication authResult;try {// 开始认证,attemptAuthentication 在 TokenLoginFilter 中实现,继承了UsernamePasswordAuthenticationFilterauthResult = attemptAuthentication(request, response);// return null 认证失败if (authResult == null) {return;}// 调用 UsernamePasswordAuthenticationFilter 子类 TokenLoginFilter 登录成功的方法successfulAuthentication(request, response, chain, authResult);}

TokenLoginFilter继承了UsernamePasswordAuthenticationFilter,会执行此attemptAuthentication方法。

方法流程:

将用户名和密码封装到UsernamePasswordAuthenticationToke(Authentication的实现类),调用AuthenticationManager的authenticate方法进行认证。

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {private AuthenticationManager authenticationManager;private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {this.authenticationManager = authenticationManager;this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;// 关闭登录只允许 POST请求this.setPostOnly(false);// 设置登陆路径,并且为 POST 请求this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));}/*** 获取登录页传递过来的账号和密码信息*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)throws AuthenticationException {try {User user = new ObjectMapper().readValue(req.getInputStream(), User.class);// 进行用户认证// AuthenticationManager.authenticate -> AbstractUserDetailsAuthenticationProvider -> UserDetails 返回用户信息和权限信息// 密码正确,将权限信息封装到Authentication <- 通过 PasswordEncoder和 UserDetails中的密码对比 <-// 它会返回一个新的 UsernamePasswordAuthenticationToken 对象,并将权限信息赋值给 authorities 属性,// 将返回的 UserDetails对象 赋值给 principal 属性return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));} catch (IOException e) {throw new RuntimeException(e);}}/*** 登录成功*/@Overrideprotected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,Authentication auth) throws IOException, ServletException {SecurityUser user = (SecurityUser) auth.getPrincipal();// 根据用户名创建 tokenString token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());// 以用户名为 key,权限信息为 value 存入 redisredisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());// 将token返回给前端ResponseUtil.out(res, R.ok().data("token", token));}/*** 登录失败*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException e) throws IOException, ServletException {ResponseUtil.out(response, R.error());}
}
public class ResponseUtil {public static void out(HttpServletResponse response, R r) {ObjectMapper mapper = new ObjectMapper();// 设置响应编码 200response.setStatus(HttpStatus.OK.value());// 设置请求体的编码格式:application/json;charset=UTF-8// Content-Type:设置请求体的编码格式response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);try {// 将此信息通过 Json 形式发送给前端mapper.writeValue(response.getWriter(), r);} catch (IOException e) {e.printStackTrace();}}
}

2.3 AuthenticationManager

这里的AuthenticationManager具体实现类为ProviderManager,可以通过debug查看出来。

主要流程:

如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,成功验证后,将不会尝试后续的AuthenticationProvider。

我们也可以自定义 AuthenticationProvider 完成自定义认证。

// 验证 Authentication 对象(里面包含着验证对象)
public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class toTest = authentication.getClass();AuthenticationException lastException = null;AuthenticationException parentException = null;Authentication result = null;Authentication parentResult = null;boolean debug = logger.isDebugEnabled();// 如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,// 那么由第一个来确定结果,成功验证后,将不会尝试后续的AuthenticationProvider。for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}if (debug) {logger.debug("Authentication attempt using "+ provider.getClass().getName());}try {result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);// 结束 for 循环break;}}catch (AccountStatusException | InternalAuthenticationServiceException e) {prepareException(e, authentication);throw e;} catch (AuthenticationException e) {lastException = e;}}// result 等于 null, 并且 parent 不等于 null,调用父类的 parent.authenticate 方法if (result == null && parent != null) {try {result = parentResult = parent.authenticate(authentication);}catch (ProviderNotFoundException e) {}catch (AuthenticationException e) {lastException = parentException = e;}}// result 不等于 null,返回 resultif (result != null) {if (eraseCredentialsAfterAuthentication&& (result instanceof CredentialsContainer)) {((CredentialsContainer) result).eraseCredentials();}if (parentResult == null) {eventPublisher.publishAuthenticationSuccess(result);}return result;}if (lastException == null) {lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",new Object[] { toTest.getName() },"No AuthenticationProvider found for {0}"));}if (parentException == null) {prepareException(lastException, authentication);}throw lastException;
}

2.4 AuthenticationProvider

这里AuthenticationProvider的具体实现类为 AbstractUserDetailsAuthenticationProvider,可以通过debug查看出来。

流程:

  1. 从 authentication 中获取到用户名

  1. 尝试从缓存中获取用户信息,没有获取到则调用retrieveUser方法检索用户信息,retrieveUser调用 实际调用的是UserDetailsService 的 loadUserByUsername 方法加载用户信息及其权限信息。

  1. 对获取到的用户信息进行验证,判断帐号是否锁定\是否禁用\帐号是否到期,实际调用的是UserDetails的 isAccountNonLocked、isAccountNonExpired等方法,验证不通过会抛出异常。

  1. 验证密码是否正确,实际调用的是PasswordEncoder的matches方法,密码错误抛出AuthenticationException

  1. 如果在对用户信息,密码验证的过程中抛出异常,此时会判断用户信息是否从缓存中得到,考虑到数据是不实时的,重新通过retrieveUser方法去取出用户信息,再次重复进行检查验证

  1. 判断用户密码是否过期,过期则抛出异常

  1. 将用户信息放入缓存

  1. 返回封装好的UsernamePasswordAuthenticationToken对象,

  1. principal:用户名

  1. credentials:密码

  1. authorities:权限信息

public Authentication authenticate(Authentication authentication)throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,() -> messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));// 确认用户名String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();// 是否从缓存中取出用户信息boolean cacheWasUsed = true;// 从缓存中读取用户信息UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {// 设置不是从缓存中获取信息cacheWasUsed = false;try {// 根据用户名检索用户信息user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}catch (UsernameNotFoundException notFound) {logger.debug("User '" + username + "' not found");if (hideUserNotFoundExceptions) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}else {throw notFound;}}Assert.notNull(user,"retrieveUser returned null - a violation of the interface contract");}try {// 验证帐号是否锁定\是否禁用\帐号是否到期preAuthenticationChecks.check(user);// 验证密码是否正确,不然抛出AuthenticationExceptionadditionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}catch (AuthenticationException exception) {// 调用某个UserDetailsChecker接口的实现类验证失败后,就判断下用户信息是否从内存中得到,// 如果之前是从内存中得到的用户信息,那么考虑到可能数据是不实时的,// 就重新通过retrieveUser方法去取出用户信息,再次重复进行检查验证if (cacheWasUsed) {cacheWasUsed = false;user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}else {throw exception;}}// 判断用户的密码是否过期postAuthenticationChecks.check(user);// 如果没有缓存用户信息,则使用userCache缓存。if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (forcePrincipalAsString) {// 获取用户名principalToReturn = user.getUsername();}// 返回封装后的 UsernamePasswordAuthenticationTokenreturn createSuccessAuthentication(principalToReturn, authentication, user);
}protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {// 参数一:用户名// 参数二:密码// 参数三:权限信息UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());return result;
}

authentication的getName方法,会根据不同的接口调用不同的方法获取用户名,我们上面传递过来的只是一个字符串,所以最终调用 toString() 方法返回用户名。

public String getName() {if (this.getPrincipal() instanceof UserDetails) {return ((UserDetails) this.getPrincipal()).getUsername();}if (this.getPrincipal() instanceof AuthenticatedPrincipal) {return ((AuthenticatedPrincipal) this.getPrincipal()).getName();}if (this.getPrincipal() instanceof Principal) {return ((Principal) this.getPrincipal()).getName();}return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}

DaoAuthenticationProvider类:

protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {// 调用 UserDetailsService 的 loadUserByUsername 方法加载用户信息及其权限信息UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}
}protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {// 凭证信息为 null ,抛出异常if (authentication.getCredentials() == null) {logger.debug("Authentication failed: no credentials provided");// BadCredentialsException 继承于 AuthenticationExceptionthrow new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}// 获取凭证信息(密码)String presentedPassword = authentication.getCredentials().toString();// 调用 passwordEncoder 匹配密码是否正确if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}
}

AccountStatusUserDetailsChecker类

public void check(UserDetails user) {if (!user.isAccountNonLocked()) {throw new LockedException(messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));}if (!user.isEnabled()) {throw new DisabledException(messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));}if (!user.isAccountNonExpired()) {throw new AccountExpiredException(messages.getMessage("AccountStatusUserDetailsChecker.expired","User account has expired"));}if (!user.isCredentialsNonExpired()) {throw new CredentialsExpiredException(messages.getMessage("AccountStatusUserDetailsChecker.credentialsExpired","User credentials have expired"));}
}

DefaultPostAuthenticationChecks类

private class DefaultPostAuthenticationChecks implements UserDetailsChecker {public void check(UserDetails user) {// 密码是否过期if (!user.isCredentialsNonExpired()) {logger.debug("User account credentials have expired");throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired","User credentials have expired"));}}
}

2.5 PasswordEncoder

PasswordEncoder 是 Security 提供的一个接口,密码加密器

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {/*** 对字符串加密* @param rawPassword* @return*/@Overridepublic String encode(CharSequence rawPassword) {return MD5.encrypt(rawPassword.toString());}/*** 校验传入的明文密码 rawPassword 是否和加密密码 encodedPassword 相匹配* @param rawPassword* @param encodedPassword* @return*/@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));}}

2.6 UserDetailsService

加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。

我们自定义实现的 UserDetailsService 类

流程:

  1. 根据username从数据库中取出用户信息,判断用户是否存在,不存在抛出异常

  1. 根据 userid 从数据库获取权限信息,封装到 securityUser 返回

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserService userService;@Autowiredprivate PermissionService permissionService;/**** 根据账号获取用户信息*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 从数据库中取出用户信息User user = userService.selectByUsername(username);// 2. 判断用户是否存在if (null == user){throw new UsernameNotFoundException("用户名不存在!");}// 3. 返回 UserDetails 实现类com.atguigu.security.entity.User curUser = new com.atguigu.security.entity.User();BeanUtils.copyProperties(user, curUser);// 根据 userid 获取权限信息,封装到 securityUser 返回List authorities = permissionService.selectPermissionValueByUserId(user.getId());SecurityUser securityUser = new SecurityUser(curUser);securityUser.setPermissionValueList(authorities);return securityUser;}
}

2.7 UserDetails

提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中。

/*** UserDetails接口:提供核心用户信息*/
public class SecurityUser implements UserDetails {//当前登录用户,transient 修饰,不参与序列化过程private transient User currentUserInfo;//当前权限private List permissionValueList;public SecurityUser() {}public SecurityUser(User user) {if (user != null) {this.currentUserInfo = user;}}/*** 获取权限信息* @return*/@Overridepublic Collection getAuthorities() {Collection authorities = new ArrayList<>();// 把 permissionValueList 中 String 类型的权限信息封装成 SimpleGrantedAuthorityfor(String permissionValue : permissionValueList) {if(StringUtils.isEmpty(permissionValue)) {continue;}SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);authorities.add(authority);}return authorities;}@Overridepublic String getPassword() {return currentUserInfo.getPassword();}@Overridepublic String getUsername() {return currentUserInfo.getUsername();}/*** 帐户是否未过期* @return*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 帐户是否未锁定* @return*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 凭证是否未过期* @return*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 用户是否可用* @return*/@Overridepublic boolean isEnabled() {return true;}
}

2.8 配置信息

@Configuration
// 开启 SpringSecurity 的默认行为
@EnableWebSecurity
// 开启基于方法的安全认证机制,也就是说在 Web 层的 Controller 启用注解机制的安全确认,
// 如 @PreAuthorize(“hasAuthority(‘admin’)”) 才会生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {private UserDetailsService userDetailsService;private TokenManager tokenManager;private DefaultPasswordEncoder defaultPasswordEncoder;private RedisTemplate redisTemplate;@Autowiredpublic TokenWebSecurityConfig(UserDetailsService userDetailsService,DefaultPasswordEncoder defaultPasswordEncoder,TokenManager tokenManager, RedisTemplate redisTemplate) {this.userDetailsService = userDetailsService;this.defaultPasswordEncoder = defaultPasswordEncoder;this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.exceptionHandling()// 认证失败的异常处理器.authenticationEntryPoint(new UnauthorizedEntryPoint()).and()// 关闭 csrf.csrf().disable().authorizeRequests()// 任何请求都需要认证.anyRequest().authenticated().and()// 设置登出地址.logout().logoutUrl("/admin/acl/index/logout")// 添加登出业务逻辑类.addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate)).and()// 添加登录过滤器.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))// 添加token过滤器.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();}/*** 设置 自定义的密码加密器 和 自定义加载用户特定数据的类* @param auth* @throws Exception*/@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);}/*** 配置哪些请求不拦截* @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/api/**","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");}
}

说明:

CSRF攻击依靠的是Cookie中所携带的认证信息,但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中Cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

3、成功登录

前端会将后端返回的token信息保存在Cookie中,每次请求都会携带该信息。

4、退出登录

主要流程:

从请求中获取到token,解析得到username,从redis删除以 userName 为 key 的键值对。

对于token信息,是由前端执行删除的。

public class TokenLogoutHandler implements LogoutHandler {private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}/*** 定义登出功能, 主要是执行一些必要的清理,这些类不应该抛出异常*/@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {// 1. 从请求头获取 tokenString token = request.getHeader("token");if (token != null) {// 2. 从 token 中解析出 userNameString userName = tokenManager.getUserFromToken(token);// 3. 从 redis 删除以 userName 为 key 的键值对redisTemplate.delete(userName);// 4. 将 R 以 json 形式发送给前端ResponseUtil.out(response, R.ok());}}
}

正常请求的分析

每次请求接口,请求头携带token,后台通过自定义 token 过滤器拦截解析 token 完成认证并填充用户信息实体

流程:

  1. 首先经过UsernamePasswordAuthenticationFilter,由于不是登录请求,会直接放行

  1. 来到 TokenAuthenticationFilter,从请求头中拿到token信息,解析得到 username

  1. 从 redis 获取到权限信息,封装到 UsernamePasswordAuthenticationToken

  1. 将身份验证信息存入 SecurityContextHolder,后续需要获取可以通过如下方式。

    @GetMapping("info")public R info(){//获取当前登录用户用户名String username = SecurityContextHolder.getContext().getAuthentication().getName();Map userInfo = indexService.getUserInfo(username);return R.ok().data(userInfo);}
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {super(authManager);this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}@Overrideprotected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)throws IOException, ServletException {logger.info("=================" + req.getRequestURI());// 路径中含有 admin 则不拦截if(req.getRequestURI().indexOf("admin") == -1) {chain.doFilter(req, res);return;}UsernamePasswordAuthenticationToken authentication = null;try {// 获取身份验证信息authentication = getAuthentication(req);} catch (Exception e) {ResponseUtil.out(res, R.error());}if (authentication != null) {// 将身份验证信息存入 SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(authentication);} else {ResponseUtil.out(res, R.error());}chain.doFilter(req, res);}/*** 获取请求头的 token 信息* @param request* @return*/private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {// 从请求头中取出 headerString token = request.getHeader("token");if (StringUtils.hasText(token)) {// 根据 token 获取用户名String userName = tokenManager.getUserFromToken(token);// 从 redis 获取用户权限信息List permissionValueList = (List) redisTemplate.opsForValue().get(userName);// 把 permissionValueList 中 String 类型的权限信息封装成 SimpleGrantedAuthorityCollection authorities = new ArrayList<>();for(String permissionValue : permissionValueList) {if(StringUtils.isEmpty(permissionValue)) {continue;}SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);authorities.add(authority);}if (!StringUtils.isEmpty(userName)) {// 返回 UsernamePasswordAuthenticationToken 对象return new UsernamePasswordAuthenticationToken(userName, token, authorities);}return null;}return null;}
}

相关内容

热门资讯

基于深度学习的车型识别系统(P... 摘要:基于深度学习的车型识别系统用于识别不同类型的车辆,应用YOLO V...
[C语言]错误信息报告stre... strerrorchar* strerror(int errnum);//返回错误码,...
配置管理的四个要点 配置管理是基础,是关键。做工具平台或系统,一定要重视基础的建设。一定要做...
机构密集调研!长三角区域银行为... 地方性上市银行大获青睐。 5月29日,《国际金融报》记者梳理Wind数据发现,已有超千家机构对24家...
Android文件目录 前言 Android文件目录可以分为内部存储和外部存储,其中外部存储又可以分为私有目录...
用友NC数据库迁移服务解决方案 NC数据库迁移工具操作说明          NC数据库迁移工具的功能:不同数据库之间...
Harbor仓库开启SSL 目录一、部署docker二、安装docker-compose三、安装harbor下载离线安装包&#x...
HTML、CSS学习笔记7(媒... 目录 一、媒体查询 1.max-min-width 2.书写顺序  3.媒体类型 4.媒体查询——l...
Word加载项/插件管理 Word加载项/插件管理前言准备:开启加载项功能区加载项Word加载项COM加载项 前...
Spring产生数据源对象(c... 目录一、c3p01. 添加依赖2. 在resources中创建applicationContext....
ArcScene制作三维地图-... ArcScene制作三维地图-三维模型发布时间:2018-01-17 版权࿱...
【C++】面试101,用两个栈... 目录 1. 用两个栈实现队列  2.包含min函数的栈 3.有效括号序列  4.滑动窗口的最大值 5...
一文说明白,TypeScrip... ● 通过前两章的学习, 我们基本上对于 TS 已经入门了 ● 但是我们会发现, 我们好像对于类型限制...
《JavaEE初阶》计算机网络... 《JavaEE初阶》计算机网络之网络原理(传输层) 前言: 本章主要将介绍传输层的UDP与TCP协议...
菲林格尔筹划控制权变更事项 6... 菲林格尔(603226)筹划易主。 5月30日晚间菲林格尔公告,公司于5月30日收到公司实际控制人丁...
[实战]上传文件的实战问题 [实战]上传文件的实战问题文件上传前的编辑问题上传文件的时机选择=>编辑=>上传 文...
学习笔记:基于SpringBo... 一、发送邮件 发送邮件主要使用JavaMailSender类,使用send方法&#x...
ENet学习笔记 ENet:A deep Neural Network Architecture for Real-T...
Obsidian插件推荐和页面... 1. 插件推荐 custom attachment location 可自定义附件图片的存放位置&#...
7个Python中的隐藏小技巧... Python 是每个程序员都喜欢的语言,因为它易于编码和易于阅读的语法。但是ÿ...
Kotlin实现Service... Service的使用1异步通信方式2Service的启动和停止3希望Activity和Service...
python绘制三维图 一、初始化 假设已经安装了matplotlib工具包。 利用matplotlib.figure.Fi...
每周股票复盘:恺英网络(002... 截至2025年5月30日收盘,恺英网络(002517)报收于15.93元,较上周的15.77元上涨1...
HashTabld底层源码解读 Java源码系列:下方连接 http://t.csdn.cn/Nwzed 文章目录...
常熟银行新注册《常熟农商银行P... 证券之星消息,近日常熟银行(601128)新注册了《常熟农商银行PDF转Word系统V1.0.0.0...
血洗币圈!比特币崩盘,跌至8万... 端午安康,祝大家在牛市中稳健操作,迈向财富自由!别忘了点赞关注,咱们一起把握市场机会! 比特币最近...
k8s部署prometheus k8s部署prometheus 版本说明: k8s:1.24.4 pro...
OpenAI似乎步子迈太大了 ... 出品|虎嗅科技组作者|孙晓晨编辑|苗正卿头图|视觉中国当地时间5月29日,据外媒报道,特拉华州总检察...
代码随想录-58-654.最大... 目录前言题目1.构造二叉树-递归(区间,左闭右开)2. 本...
Git Actions自动发布... Git Actions自动发布部署,非最完善但足够完善和上手的一篇 文章最后附带完整...