前端会调用此接口去实现登录
// 登录
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
包含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/**
以下是SpringSecurity的用户认证流程,下面我会详细分析
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念:当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的,这个不难理解,要是当前登录用户都不能确认了,那A下了一个订单,下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。
我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取Authentication,SecurityContext就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程中传递同一个对象!
现在我们已经知道了Spring Security中三个核心组件:
Authentication:存储了认证信息,代表当前登录用户
SeucirtyContext:上下文对象,用来获取Authentication
SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext
Authentication中是什么信息?
Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
Credentials:用户凭证,一般是密码
Authorities:用户权限
用户在提交完用户名和密码后,请求会首先来到 UsernamePasswordAuthenticationFilter,执行其 doFilter方法。
这个方法 UsernamePasswordAuthenticationFilter 并没有实现,而是其父类AbstractAuthenticationProcessingFilter 实现的。
流程如下:
判断是否是登陆的请求,不是的话直接放过
调用子类的attemptAuthentication进行认证
认证成功执行successfulAuthentication方法,失败则返回
根据用户名生成token
将权限信息存入redis
给前端返回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();}}
}
这里的AuthenticationManager具体实现类为ProviderManager,可以通过debug查看出来。
主要流程:
如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,成功验证后,将不会尝试后续的AuthenticationProvider。
我们也可以自定义 AuthenticationProvider 完成自定义认证。
// 验证 Authentication 对象(里面包含着验证对象)
public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class extends Authentication> 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;
}
这里AuthenticationProvider的具体实现类为 AbstractUserDetailsAuthenticationProvider,可以通过debug查看出来。
流程:
从 authentication 中获取到用户名
尝试从缓存中获取用户信息,没有获取到则调用retrieveUser方法检索用户信息,retrieveUser调用 实际调用的是UserDetailsService 的 loadUserByUsername 方法加载用户信息及其权限信息。
对获取到的用户信息进行验证,判断帐号是否锁定\是否禁用\帐号是否到期,实际调用的是UserDetails的 isAccountNonLocked、isAccountNonExpired等方法,验证不通过会抛出异常。
验证密码是否正确,实际调用的是PasswordEncoder的matches方法,密码错误抛出AuthenticationException
如果在对用户信息,密码验证的过程中抛出异常,此时会判断用户信息是否从缓存中得到,考虑到数据是不实时的,重新通过retrieveUser方法去取出用户信息,再次重复进行检查验证
判断用户密码是否过期,过期则抛出异常
将用户信息放入缓存
返回封装好的UsernamePasswordAuthenticationToken对象,
principal:用户名
credentials:密码
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"));}}
}
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()));}}
加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
我们自定义实现的 UserDetailsService 类
流程:
根据username从数据库中取出用户信息,判断用户是否存在,不存在抛出异常
根据 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;}
}
提供核心用户信息。通过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 extends GrantedAuthority> 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;}
}
@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攻击也就不用担心了。
前端会将后端返回的token信息保存在Cookie中,每次请求都会携带该信息。
主要流程:
从请求中获取到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 完成认证并填充用户信息实体
流程:
首先经过UsernamePasswordAuthenticationFilter,由于不是登录请求,会直接放行
来到 TokenAuthenticationFilter,从请求头中拿到token信息,解析得到 username
从 redis 获取到权限信息,封装到 UsernamePasswordAuthenticationToken
将身份验证信息存入 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;}
}