SpringSecurity配置

SecurityConfig.java

@Override
protected void configure(HttpSecurity http) throws Exception {
// CRSF禁用,不使用session
http.csrf().disable();
// 基于token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 过滤请求
http.authorizeRequests()
.antMatchers("/login", "/captchaImage").anonymous()
.anyRequest().authenticated();
....
// 添加JWT filter
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

从上面可以看到我们设置了放行 /login/captchaImage 请求,而其他请求就需要通过认证才可访问。下面正式分析登录认证流程

登录认证

首先分析一下流程

1、前端填写完表单数据后,发送请求 [post] /login,传递 username、password、code、uuid这四个数据

2、后端收到 [post] /login请求,来到相应的 controller 处理方法

@RestController
public class SysLoginController { @Autowired
private SysLoginService loginService; @PostMapping("login")
public Result login(String username, String password, String code, String uuid){
Result result = Result.success();
String token = loginService.login(username, password, code, uuid);
result.put(Constants.TOKEN, token);
return result;
}

在这里调用 loginService 的 login方法,其中内部是具体的验证登录逻辑,验证通过后返回一个token,然后回传给前端。前端这时就可以将token数据存入本地了

3、接下来进入 loginService.login()内,进行详细分析

@Component
public class SysLoginService {
@Autowired private RedisCache redisCache;
@Autowired private TokenService tokenService;
@Autowired private AuthenticationManager authenticationManager; public String login(String username, String password, String code, String uuid) {
String verifyKey = Constants.CAPTCHA_CODE_TAG + uuid;
String verifyCode = redisCache.getCacheObject(verifyKey);
...省去验证码校验逻辑,校验失败会抛出异常 Authentication authenticate = null;
try {
// 认证 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
throw new UserPasswordNotMatchException();
} else {
throw new CustomException(e.getMessage());
}
}
return tokenService.createToken((LoginUser) authenticate.getPrincipal());
}
}

3.1 首先进行验证码校验逻辑,不通过时会抛出异常

3.2 通过 AuthenticationManagerauthenticate()获取认证信息

下面来详细分析一下 authenticationManager.authenticate()认证过程

分析之前先来了解一下 UsernamePasswordAuthenticationToken 这个类,它拥有两个构造方法

// 1、只有两个参数的构造方法表示[当前没有认证]
public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
// 2、拥有三个参数的构造方法表示[当前已经认证完毕]
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)

下面开始分析认证过程

1)实现了AuthenticationManangerProviderManger调用接口的authenticate方法

2)然后遍历所有的 AuthenticProvider,其中的supports方法,返回时一个boolean值,参数是一个Class,就是根据Token的类来确定用什么Provider来处理

而源码中的toTest类,就是我们认证传递的 UsernamePasswordAuthenticationToken,而他对应的provider就是AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider.java

@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}

3)找到合适的Provider后,在本例中也即是AbstractUserDetailsAuthenticationProvider(抽象类),会调用provider 的authenticate 方法

4)从下面可以看到 retrieveUser 方法返回一个 UserDetails

5)接着深入,可以发现 DaoAuthenticationProvider 继承了 AbstractUserDetailsAuthenticationProvider,所以DaoAuthenticationProvider 才是真正的实现类,他会调用 retrieveUser 方法,接着调用 loaderUserByUsername() 方法

看到 loaderUserByUsername(),应该就很熟悉了,因为这就是我们自己实现 UserDetailsService 接口,自定义的认证过程

6)接着看我们自定义的 UserDetailServiceImpl

@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
...
@Autowired private ISysUserService userService;
@Autowired private SysPermissionService permissionService; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userService.selectUserByUserName(username);
if (null == user) {
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
} public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user, permissionService.getMenuPermission(user));
}
}

上面就是我们自己的认证逻辑。通过一个唯一标识查询用户,在这里就是username,当所有校验都通过后就会调用 createLoginUser 方法,装填用户拥有的权限以及从数据库中获取的密码,返回一个 LoginUser 对象,而这个对象实现了 UserDetails接口。(创建token的方法以及LoginUser.java可以参考文末的相关代码)

7)然后我们回看AbstractUserDetailsAuthenticationProvider 的 authenticate 方法

8)接着深入,可以发现createSuccessAuthentication方法创建了一个UsernamePasswordAuthenticationToken,并且他的构造方法有三个参数,这表明这个token是已近认证过后的

4、至此认证已经结束,我再回到 loginService.login()这个我们自己写的方法内,上面分析的8个步骤,也就是调用 authenticationManager.authenticate()的过程会返回一个 Authentication,然后就可以利用这个 Authentication 生成一个 token。

loginService.java

return tokenService.createToken((LoginUser) authenticate.getPrincipal());

5、接着回到前面第二步controller调用的 loginService.login(),这时它已近拿到了 token ,于是将其返回到前端。前端收到相应后,就可以把这个token存在本地,以后每次访问请求时都带上这个token 信息。

@PostMapping("login")
public AjaxResult login(String username, String password, String code, String uuid){
AjaxResult result = AjaxResult.success();
String token = loginService.login(username, password, code, uuid);
result.put(Constants.TOKEN, token);
return result;
}

请求认证

1、上面说到前端每次发送请求都带上这个 token,但是为啥带上这个 token,SpringSecurity就会认为此次请求已近认证通过了呢,别忘了,因为之前配置 SecurityConfig,如下

public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// 过滤请求
http.authorizeRequests()
.antMatchers("/login", "/captchaImage").anonymous()
.anyRequest().authenticated();
// 添加JWT filter,后面马上就讲
http.addFilterBefore(authenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
}

因为我们设置了放行 /login请求,所以才没遭受拦截,而其他请求都是要被拦截的

2、因此我们就要自定义一个JWT filter,使用 addFilterBefore 把它添加到过滤器列表中,下面来看我们写的 JWT过滤器

JwtAuthenticationTokenFilter.java

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired
private TokenService tokenService; @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 1)
LoginUser loginUser = tokenService.getLoginUser(request);
// 2)
if (ObjectUtil.isNotNull(loginUser) && ObjectUtil.isNull(SecurityUtils.getAuthentication()))
{
// 3.1)
tokenService.verifyToken(loginUser);
// 3.2)
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}

1)TokenService#getLoginUser() 是我们编写的一个方法,可以从 request 中获取 token,然后再解析成LoginUser,也就是我们之前编写继承了UserDetails的类

2)如果1)能解析成功,也就是loginUser不为空,就说明请求含带了token认证信息,又因为这是个前后端分离项目,SecurityUtils.getAuthentication()肯定获取不到当前请求的认证信息

public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}

3)而没有认证信息,这次请求势必会被拦截下来,所以我们要手动加上这个认证信息

3.1)我们先刷新一下token,也就是在redis缓存中更新一下到期时间(刷新token的方法可以参考文末的相关代码)

3.2)然后创建UsernamePasswordAuthenticationToken,注意这是个有三个 参数的构造方法,前面也说了,这代表已经经过认证,然后 SecurityContextHolder.getContext().setAuthentication();设置一下认证信息

这样每次带token的请求,都会有了认证信息,也就不会被拦截了

3、

但是为了更深刻的了解,我们接下来具体分析一下流程。

再次之前先介绍一个类 FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤链的最底部,下面来看看源码。

下面来打个断点,查看一下

这说明来到beforeInvocation方法时我们前面编写的jwtFilter已经被执行,认证信息已近被手动添加过了

进入beforeInvocation()里面,由调试信息可以看到当前请求需要被认证

接着我们进入authenticateIfRequired方法的内部

因为我们之前jwtfilter手动添加了认证信息,所以authenticateIfRequired就直接返回了authentication,

否则的还要进行authenticationManager.authenticate();进行验证,如果没有之前jwtfilter手动添加认证信息,那么中途一定会抛出异常,导致此次请求失败被拦截

至此也没啥好讲了,filterInvocation.getChain().doFilter() 调用我们的后台服务了

相关代码

TokenService.java

@Component
public class TokenService { protected static final long MILLIS_SECOND = 1000; protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; private static final long MILLIS_MINUTE_20 = 20 * 60 * 1000L; // 令牌自定义标识
@Value("${token.header}")
private String header; // 令牌秘钥
@Value("${token.secret}")
private String secret; // 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime; @Autowired
private RedisCache redisCache; /**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
String token = IdUtil.fastUUID();
loginUser.setToken(token);
refreshToken(loginUser); Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_TOKEN_KEY, token);
return createToken(claims);
} /**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser 登录用户
* @return 令牌
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_20) {
refreshToken(loginUser);
}
} /**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
} /**
* 获取 token 的 redis 键前缀
*
* @param uuid
* @return
*/
private String getTokenKey(String uuid) {
return Constants.LOGIN_TOKEN_TAG + uuid;
} /**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
} /**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} /**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getRespToken(request);
if (StringUtils.isNotEmpty(token)) {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_TOKEN_KEY);
String userKey = getTokenKey(uuid);
return redisCache.getCacheObject(userKey);
}
return null;
} /**
* 获取请求token
*
* @param request
* @return token
*/
private String getRespToken(HttpServletRequest request) {
String token = request.getHeader(this.header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.REQ_TOKEN_PREFIX)) {
token = token.replace(Constants.REQ_TOKEN_PREFIX, "");
}
return token;
}
}

LoginUser.java

public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1821121071052157802L; /** 用户唯一标识 */
private String token; /** 登陆时间 */
private Long loginTime; /** 过期时间 */
private Long expireTime;
...
/** 权限列表 */
private Set<String> permissions; /** 用户信息 */
private SysUser user;
...
@Override
public String getPassword() {
return user.getPassword();
} @Override
public String getUsername() {
return user.getUserName();
}
...

参考

https://www.cnblogs.com/ymstars/p/10626786.html

https://www.jianshu.com/p/d5ce890c67f7

https://gitee.com/y_project/RuoYi-Vue

SpringSecurity认证流程的更多相关文章

  1. SpringSecurity认证流程详解

    SpringSecurity基本原理 在之前的文章<SpringBoot + Spring Security 基本使用及个性化登录配置>中对SpringSecurity进行了简单的使用介绍 ...

  2. 手把手带你撸一把springsecurity框架源码中的认证流程

    提springsecurity之前,不得不说一下另外一个轻量级的安全框架Shiro,在springboot未出世之前,Shiro可谓是颇有统一J2EE的安全领域的趋势. 有关shiro的技术点 1.s ...

  3. Spring Security构建Rest服务-0701-个性化用户认证流程

    上一篇说了用户认证的基本流程,但是上一篇当访问一个受保护的服务后,如果未认证会调到默认的登录页面,这样是不行的,而且认证成功后,就直接访问了那个服务,如果想要做认证成功后做一些操作,还需要自定义. 个 ...

  4. CWMP开源代码研究4——认证流程

    TR069 Http Digest 认证流程   一 流程及流程图 1.1盒端主动发起Http Digest认证流程  盒端CPE                                    ...

  5. Kerberos认证流程详解

    Kerberos是诞生于上个世纪90年代的计算机认证协议,被广泛应用于各大操作系统和Hadoop生态系统中.了解Kerberos认证的流程将有助于解决Hadoop集群中的安全配置过程中的问题.为此,本 ...

  6. Shiro第二篇【介绍Shiro、认证流程、自定义realm、自定义realm支持md5】

    什么是Shiro shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证.用户授权. spring中有spring security (原名Acegi),是一个权限框架,它和sp ...

  7. 最简单易懂的Spring Security 身份认证流程讲解

    最简单易懂的Spring Security 身份认证流程讲解 导言 相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,其实这是一个误区,Spring Security确 ...

  8. ASP.NET Forms 认证流程

    ASP.NET Forms 认证 Forms认证基础 HTTP是无状态的协议,也就是说用户的每次请求对服务器来说都是一次全新的请求,服务器不能识别这个请求是哪个用户发送的. 那服务器如何去判断一个用户 ...

  9. 二、django rest_framework源码之认证流程剖析

    1 绪言 上一篇中讲了django rest_framework总体流程,整个流程中最关键的一步就是执行dispatch方法.在dispatch方法中,在调用了一个initial方法,所有的认证.权限 ...

随机推荐

  1. 【JAVA并发第三篇】线程间通信

    线程间的通信 JVM在运行时会将自己管理的内存区域,划分为不同的数据区,称为运行时数据区.每个线程都有自己私有的内存空间,如下图示: Java线程按照自己虚拟机栈中的方法代码一步一步的执行下去,在这一 ...

  2. 如何创建一个 PostgreSQL 数据库?

    PostgreSQL 官网截图 PostgreSQL 是什么? PostgreSQL 是一个功能非常强大的,历史悠久,开源的关系数据库.PostgreSQL支持大部分的SQL标准并且提供了很多其他现代 ...

  3. 阿里云RDS物理备份恢复到本地

    一:业务场景 验证阿里云备份文件可用性 二:恢复到本地过程中遇到的问题 1.修改密码报错 2.自定义函数不可用 三:恢复步骤 1.xtrabackup安装使用 请参考:https://www.cnbl ...

  4. JVM(七)字符串详解

     常量池: 我们前面也一直说常量池有三种: 1:class文件中的常量池,前面我们解析class文件的时候解析的就是,这是静态常量池.在硬盘上. 2:运行时常量池.可以通过HSDB查看,是Instan ...

  5. Java编程技术之浅析SPI服务发现机制

    SPI服务发现机制 SPI是Java JDK内部提供的一种服务发现机制. SPI->Service Provider Interface,服务提供接口,是Java JDK内置的一种服务发现机制 ...

  6. flume到底会丢数据吗?其可靠性如何?——轻松搞懂Flume事务机制

    先给出答案: 需要结合具体使用的source.channel和sink来分析,具体结果可看本文最后一节. Flume事务   一提到事务,我们首先就想到的是MySQL中的事务,事务就是将一批操作做成原 ...

  7. memset 在c++中使用细节注意

    C语言,在利用struct进行数据封装时,经常会使用memset(this,0,sizeof(*this))来初始化.而C++中,有时候也会用到struct,在利用memset进行初始化时,非常容易踩 ...

  8. Webpack4.0各个击破(8)tapable篇

    目录 一. tapable概述 二. tapable-0.2源码解析 2.1 代码结构 2.2 事件监听方法 2.3 事件触发方法 三. tapable1.0概述 一. tapable概述 tapab ...

  9. MVC架构 项目实践

    MVC MVC架构程序的工作流程 springmvc 中dao层和service层的区别 项目实践 项目目录 项目实现流程 JSP登录页面View层 LoginServletjavaControlle ...

  10. 函数式编程(json、pickle、shelve)

    本节内容 前言 json模块 pickle模块 shelve模块 总结 一.前言 1. 现实需求 每种编程语言都有各自的数据类型,其中面向对象的编程语言还允许开发者自定义数据类型(如:自定义类),Py ...