谷粒学院SpringSecurity认证流程详解

登录功能前端分析

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

// 登录
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值

以下是我成功登录的截图

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

登录后端分析

1、网关

包含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的用户认证流程,下面我会详细分析

  • 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 中实现,继承了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查看出来。

流程:

  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);
        // 验证密码是否正确,不然抛出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 类

流程:

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

  1. 根据 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中,每次请求都会携带该信息。

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 完成认证并填充用户信息实体

流程:

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

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

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

  1. 将身份验证信息存入 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;
    }
}
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小鲁蛋儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值