登录功能前端分析
前端会调用此接口去实现登录
// 登录
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_url
timeout: 20000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
// 让每个请求携带自定义token
config.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值
![](https://i-blog.csdnimg.cn/blog_migrate/fdeb41a42fc14e71f7adb1d73f370673.png)
以下是我成功登录的截图
![](https://i-blog.csdnimg.cn/blog_migrate/e2ec8cfff058a9b95d424771169734b0.png)
最终前端的请求路径为 http://localhost:8222/admin/acl/login
登录后端分析
1、网关
![](https://i-blog.csdnimg.cn/blog_migrate/b8c02fdbfdaddb25e99cf7a6fe7dd20c.png)
包含acl的请求路径会被网关拦截,然后去注册中心找对应的服务名,再转发到对应的权限管理模块中去
spring:
application:
name: service-gateway
cloud:
#nacos服务地址
nacos:
discovery:
server-addr: 127.0.0.1:8848
#使用服务发现路由
gateway:
discovery:
locator:
enabled: true
routes:
- id: service-acl
uri: lb://service-acl
predicates:
- Path=/*/acl/**
2、SpringSecurity
以下是SpringSecurity的用户认证流程,下面我会详细分析
![](https://i-blog.csdnimg.cn/blog_migrate/c6b1643afcfc8c8769692644e365faa5.png)
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中三个核心组件:
Authentication:存储了认证信息,代表当前登录用户
SeucirtyContext:上下文对象,用来获取Authentication
SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext
Authentication中是什么信息?
Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
Credentials:用户凭证,一般是密码
Authorities:用户权限
2.2 UsernamePasswordAuthenticationFilter
用户在提交完用户名和密码后,请求会首先来到 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 中实现,继承了UsernamePasswordAuthenticationFilter
authResult = 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"));
}
/**
* 获取登录页传递过来的账号和密码信息
*/
@Override
public 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);
}
}
/**
* 登录成功
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
// 根据用户名创建 token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
// 以用户名为 key,权限信息为 value 存入 redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
// 将token返回给前端
ResponseUtil.out(res, R.ok().data("token", token));
}
/**
* 登录失败
*/
@Override
protected 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();
// 设置响应编码 200
response.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<? 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,返回 result
if (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查看出来。
流程:
从 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);
// 验证密码是否正确,不然抛出AuthenticationException
additionalAuthenticationChecks(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();
}
// 返回封装后的 UsernamePasswordAuthenticationToken
return 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 继承于 AuthenticationException
throw 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
*/
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
/**
* 校验传入的明文密码 rawPassword 是否和加密密码 encodedPassword 相匹配
* @param rawPassword
* @param encodedPassword
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
2.6 UserDetailsService
加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
我们自定义实现的 UserDetailsService 类
流程:
根据username从数据库中取出用户信息,判断用户是否存在,不存在抛出异常
根据 userid 从数据库获取权限信息,封装到 securityUser 返回
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
/***
* 根据账号获取用户信息
*/
@Override
public 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<String> 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<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
/**
* 获取权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 把 permissionValueList 中 String 类型的权限信息封装成 SimpleGrantedAuthority
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
/**
* 帐户是否未过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 帐户是否未锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 凭证是否未过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用户是否可用
* @return
*/
@Override
public 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;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService,
DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected 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
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
/**
* 配置哪些请求不拦截
* @param web
* @throws Exception
*/
@Override
public 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中,每次请求都会携带该信息。
![](https://i-blog.csdnimg.cn/blog_migrate/e2ec8cfff058a9b95d424771169734b0.png)
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;
}
/**
* 定义登出功能, 主要是执行一些必要的清理,这些类不应该抛出异常
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 1. 从请求头获取 token
String token = request.getHeader("token");
if (token != null) {
// 2. 从 token 中解析出 userName
String 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<String, Object> 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;
}
@Override
protected 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) {
// 将身份验证信息存入 SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}
chain.doFilter(req, res);
}
/**
* 获取请求头的 token 信息
* @param request
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// 从请求头中取出 header
String token = request.getHeader("token");
if (StringUtils.hasText(token)) {
// 根据 token 获取用户名
String userName = tokenManager.getUserFromToken(token);
// 从 redis 获取用户权限信息
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
// 把 permissionValueList 中 String 类型的权限信息封装成 SimpleGrantedAuthority
Collection<GrantedAuthority> 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;
}
}