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. bash5.0参考手册

    Bash Reference Manual a.summary-letter { text-decoration: none } blockquote.indentedblock { margin-r ...

  2. 使用smartform打印表单

    昨天写了个smartform打印表单,在开发完成,在测试机测试OK,传到生产机,出现严重问题!无法打印,干脆就是无法调用打印图形界面,进入SMARTFORM事物,查看这个表单,发现,居然公司的LOGO ...

  3. three.js cannon.js物理引擎地形生成器和使用指针锁定控件

    今天郭先生说一说使用cannon.js物理引擎绘制地形和使用指针锁定控件.效果如下图.线案例请点击博客原文. 这里面的生成地形的插件和指针锁定控件也是cannon.js的作者schteppe封装的,当 ...

  4. poj 1038 Bugs Integrated, Inc. 题解

    提供一种代码难度比较简单的做法(可能) 状态表示: 设置状态$ f[i][j] $,表示第 \(i\) 行状态为 \(j\) 的最大放置数,因为这是个阴间题,因为题目内存设置很小,所以要用滚动数组,存 ...

  5. CSS不用背景图片实现优惠券样式反圆角,凹圆角,反向半圆角,并且背景渐变

    日常开发过程中,特别是商城相关应用开发过程中,时常会遇到花里胡哨的设计图,比如优惠券样式,上图: 实现思路如下:     1.先写一个外容器,实现背景色渐变: Html: 1 <div clas ...

  6. eNSP启动设备AR1失败记一次解决步骤

    eNSP稳定版本下载:   微信搜索公众号"疯刘小三" 关注后回复ensp即可获得下载链接地址 eNSP V100R002C00B510 Setup.exe 最近在用eNSp的时候 ...

  7. ldf和mdf文件怎么还原到sqlserver数据库

    1.把mdf文件和ldf文件拷贝到数据库的默认路径C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA里:2.在sq ...

  8. jackson学习之四:WRAP_ROOT_VALUE(root对象)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  9. (Oracle)DDL及其数据泵导入导出(impdp/expdp)

    create tablespace ybp_dev datafile 'G:\app\Administrator\oradata\health\ybp_dev1.dbf' size 10m autoe ...

  10. python基础(int,str,bool,list)

    1数字int. 数字主要是用于计算用的,使用方法并不是很多,就记住一种就可以:      bit_length() #bit_length() 当十进制用二进制表示时,最少使用的位数 v = 11 1 ...