前言:最近在学习SpringSecurity的过程中,参考了很多网上的教程,同时也参考了一些目前主流的开源框架,于是结合自己的思路写了一个SpringBoot整合SpringSecurity+JWT+Redis完整的项目,从0到1写完感觉还是收获到不少的,于是我把我完整的笔记写成博客分享给大家,算是比较全的一个项目了,仅供大家参考和学习哦!
目录
一、SpringSecurity简介
SpringSecurity是Spring生态系统中的安全管理框架,提供了一套Web应用安全性的完整解决方案。
它具有以下特点:
1、全面性:SpringSecurity提供了认证、授权、攻击防护等安全管理的全部功能。
2、扩展性:可以通过继承类、实现接口等方式轻松扩展SpringSecurity的功能。
3、与Spring无缝集成:可以与Spring框架完美整合,通过SpringIoC容器管理SpringSecurity组件。
4、防范常见攻击:可以防止脚本注入、会话固定、SQL注入等常见Web攻击。
5、配置简单:通过配置文件可以快速应用SpringSecurity带来的安全功能。
SpringSecurity的主要功能包括:
1、认证(Authentication):验证用户身份信息的合法性。
2、授权(Authorization):验证用户是否有权限执行操作。
3、防护攻击:防御如CSRF、Session固定、SQL注入等攻击。
4、方法安全:实现与系统方法的安全访问控制。
5、安全响应头:添加浏览器安全相关的响应头,提高安全性。
综上,SpringSecurity是一个MVC应用不可或缺的安全防护框架,为Java应用提供全面的安全支持。它与Spring框架集成紧密,配置简单,使用方便。
二、SpringSecurity认证流程
Spring Security认证流程中的几个核心类及其作用如下:
1、Authentication:认证信息接口,表示当前用户的认证信息,通常使用UsernamePasswordAuthenticationToken作为实现。
2、AuthenticationManager:认证管理器接口,authenticate()方法用来执行认证流程。
3、ProviderManager:认证管理器接口常用实现,封装多个AuthenticationProvider。
4、AuthenticationProvider:具体的认证处理器,由它完成特定的认证机制。
5、UserDetailsService:根据用户名加载用户信息,返回UserDetails接口的实现。
6、UserDetails:包含用户信息的接口,框架中代表用户信息。
7、UsernamePasswordAuthenticationFilter:处理表单登录认证的过滤器。
8、AbstractAuthenticationProcessingFilter:认证处理过滤器基类。
9、SecurityContextHolder:安全上下文容器,存取Authentication对象。
这是完整的SpringSecurity的认证流程:
1、用户向系统提交用户名和密码进行认证。
2、AuthenticationFilter会拦截请求,并从请求中提取出用户名和密码构造一个UsernamePasswordAuthenticationToken。
3、AuthenticationFilter将UsernamePasswordAuthenticationToken传入AuthenticationManager。
4、AuthenticationManager会找到一个匹配的AuthenticationProvider来进行认证。
5、AuthenticationProvider会先调用UserDetailsService的loadUserByUsername()方法根据用户名加载用户信息。
6、UserDetailsService根据用户名查询数据库,构造出一个UserDetails对象,包含用户信息、权限等。
7、AuthenticationProvider使用UserDetails和用户输入的密码进行匹配验证。如果匹配上就验证成功。
8、如果验证成功,AuthenticationProvider会构造一个已认证的Authentication对象。
9、AuthenticationProvider返回Authentication给AuthenticationManager。
10、AuthenticationManager将Authentication设置到SecurityContextHolder中。
11、后续的访问控制将使用SecurityContextHolder中的Authentication信息来验证用户身份和权限。
12、登录成功,用户访问系统受保护资源。
完整流程如图所示:

三、项目核心代码讲解
因为代码量比较庞大,所以我把整个项目的关键代码单独拿出来进行讲解,其他的次要的就不贴出来了,主要还是为了能够让大家更通俗易懂的去了解SpringSecurity的执行过程,完整的代码我会开源到Gitee,提供在文章的结尾。
3.1、导入pom依赖
完整的依赖都贴出来了。
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok依赖包 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<scope>provided</scope>
</dependency>
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- redis依赖 对象池 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
<!-- 常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.34</version>
</dependency>
</dependencies>
3.2、yml配置文件
主要配置了一个Redis连接以及Token常量。
java
spring:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
token:
header: Authorization
secret: oqwe9sdladwosqwqs
expireTime: 30
3.3、实体类
一共涉及了四个实体类,主要设计了一些关键的字段,并不是非常完整。
3.3.1、登录实体类
这个类主要用于接收前端传递过来的用户名和密码,然后去验证登录信息用的。
java
package com.example.security.domain;
import lombok.Data;
@Data
public class LoginBody
{
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
}
3.3.2、角色类
存放每个用户的角色信息,=需要实现序列化接口
java
package com.example.security.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
@Data
@AllArgsConstructor
public class Role implements Serializable {
/**
* 角色主键
*/
private Long id;
/**
* 角色名称
*/
private String name;
}
3.3.3、用户类
主要存放的是用户的信息,需要实现序列化接口
java
package com.example.security.domain;
import lombok.Data;
import java.io.Serializable;
import java.util.Set;
@Data
public class User implements Serializable {
/**
* 主键
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 角色集合
*/
private Set<Role> roles;
}
3.3.4、登录用户信息
需要实现SpringSecurity自带的UserDetails接口,并实现它所有的方法,在Spring Security中,我们可以通过GrantedAuthority接口来表示一个用户所拥有的权限。
方法 | 解释 |
---|---|
isAccountNonExpired() | 账号是否已过期 |
isAccountNonLocked() | 账号是否已锁定 |
isCredentialsNonExpired() | 凭(密码)是否已过期 |
isEnabled() | 账号是否可用 |
这些方法返回true的目的是简化逻辑,在没有实现对应状态判断时,默认设置为true,这样可以避免不必要的认证/授权失败。
java
package com.example.security.domain;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
@Data
public class LoginUser implements UserDetails {
public LoginUser(User user,Set<GrantedAuthority> authorities)
{
this.user = user;
this.authorities = authorities;
}
/**
* 用户信息
*/
private User user;
/**
* 权限信息
*/
private Set<GrantedAuthority> authorities;
/**
* token信息
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3.4、TokenService服务类
先注入一些参数
java
@Value("${token.header}")
private String header;
@Value("${token.secret}")
private String secret;
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
3.4.1、生成令牌核心代码
关键代码:
java
private String generateToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
3.4.2、生成令牌关键逻辑
常量LOGIN_TOKEN_KEY:login_tokens:
1、首先生成一个随机的UUID作为token的值,设置到LoginUser对象中。
2、设置LoginUser的登录时间和过期时间(当前时间 + 过期时间)。
3、将LoginUser对象存储到Redis中,key为LOGIN_TOKEN_KEY+token,过期时间默认为yml配置的30分钟。
4、最终调用generateToken方法生成JWT token,传入claims哈希Map集合。
关键代码:
java
public String createToken(LoginUser loginUser)
{
String token = UUID.randomUUID().toString();
loginUser.setToken(token);
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
String userKey = CacheConstants.LOGIN_TOKEN_KEY + token;
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return generateToken(claims);
}
3.4.3、解析令牌核心代码
关键代码:
java
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
3.4.4、获取请求头中携带的令牌
常量TOKEN_PREFIX:"Bearer "
1、从请求header中获取指定名称(yml文件配置的header)的authorization信息。
2、判断获得的token是否非空且以指定前缀(Constants.TOKEN_PREFIX)开头。
3、如果是,则移除前缀,得到最终的JWT token。
关键代码:
java
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
3.4.5、获取Redis中存放的令牌Key
关键代码:
java
private String getTokenKey(String uuid)
{
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
3.4.6、刷新令牌有效期
1、参数loginUser是当前登录的用户信息。
2、设置loginUser的新的登录时间为当前时间。
3、重新计算过期时间为当前时间+过期时间(yml文件配置的expireTime)。
4、根据登录用户的token作为key,存储更新后的loginUser到Redis中,并设置过期时间。
5、这样就相当于刷新了token的过期时间。
关键代码:
java
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);
}
3.4.7、验证令牌有效期
验证令牌有效期,相差不足20分钟,自动刷新缓存。
关键代码:
java
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}
3.4.8、获取用户身份信息
1、先从请求中获取JWT Token。
2、如果Token不为空,则对Token进行解析,获取claims。
3、从claims中取出对应的uuid。
4、根据uuid作为key,从Redis中获取LoginUser对象。
5、如果获取成功,返回LoginUser对象。
6、如果解析token或获取用户失败,则返回null。
关键代码:
java
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
}
}
return null;
}
3.5、配置认证失败处理类
常量UNAUTHORIZED:401
AuthenticationEntryPoint是SpringSecurity中用于处理认证失败的接口,用于未登录或登录过期的情况,会触发commence方法。
1、在方法内部,首先设置了响应状态码为401 Unauthorized。
2、然后使用StringUtils生成了一个错误信息字符串,包含请求访问的接口路径和认证失败的提示。3、最后使用AjaxResult把状态码和错误信息封装成一个结果,通过ServletUtils以JSON格式写入响应中。
4、AjaxResult是一个封装AJAX请求结果的类,可以方便地生成错误或成功的响应结果。
5、ServletUtils是一个工具类,可以方便地将String数据渲染到HttpServletResponse中。
所以这个类的作用就是在认证失败时,以JSON格式返回一个包含错误代码和消息的结果到前端,前端可以根据这个结果显示对应提示或做处理。
关键代码:
java
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
3.6、配置Token认证过滤器
OncePerRequestFilter是SpringSecurity提供的一个过滤器基类,主要用于保证过滤器在一个请求内只执行一次,JwtAuthenticationTokenFilter需要继承这个基类并重写doFilterInternal的方法。
1、通过tokenService从请求头中提取JWT token,并解析得到LoginUser对象
2、调用tokenService的verifyToken方法验证JWT token的有效性
3、使用LoginUser对象构建一个UsernamePasswordAuthenticationToken
4、设置AuthenticationToken的细节,如请求来源等
5、将构造好的UsernamePasswordAuthenticationToken对象设置到SecurityContextHolder的Context中。
6、这样登录用户的Authentication对象就保存到了安全上下文中。
7、最后过滤器链继续向后执行doFilter方法。
这样在过滤器中就实现了对token的解析和验证,并设置了Authentication对象到安全上下文中,
后续的过滤器就可以依据它来判断用户认证信息了。
关键代码:
java
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Value("${token.header}")
private String header;
@Value("${token.secret}")
private String tokenKey;
@Resource
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null)
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
3.7、SecurityConfig核心配置类
这是核心代码,注释都在代码上面了,这边就不多做阐述。
关键代码:
java
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
/**
* token认证过滤器
*/
@Resource
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 认证失败处理类
*/
@Resource
private AuthenticationEntryPointImpl unauthorizedHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
//允许登录接口匿名访问
.antMatchers("/login").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
3.8、线程本地的存储
主要提供了以下几个静态方法:
1、getContext()获取当前线程的Authentication对象。
2、setContext(Authentication context)设置当前线程的Authentication对象。
3、clearContext()清除当前线程的Authentication对象。
它使用ThreadLocal维护线程隔离,所以每个线程拥有自己的Authentication信息,互不干扰。
在Spring Security中,可以通过该类在不同层传递认证信息。
关键代码:
java
public class AuthenticationContextHolder
{
private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
public static Authentication getContext()
{
return contextHolder.get();
}
public static void setContext(Authentication context)
{
contextHolder.set(context);
}
public static void clearContext()
{
contextHolder.remove();
}
}
3.9、查询用户接口
这边我偷懒了没有连接数据库,这个密码是通过如下代码加密获得的
关键代码:
java
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
主要是为了演示,所以这边直接把用户信息和角色信息写死了,实际开发还是需要连接数据库的。
关键代码:
java
@Service
public class UserServiceImpl {
public User selectUserByUsername(String username){
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setUsername(username);
user.setPassword("$2a$10$ErrO7WgkEBAWVQwuJtbBve7R2.pSKUrfs7zt8XkASqJKqcetMvAUC");
Set<Role> roles = new HashSet<>();
Role role1 = new Role(1L, "ROLE_ADMIN");
Role role2 = new Role(2L, "ROLE_USER");
roles.add(role1);
roles.add(role2);
user.setRoles(roles);
return user;
}
}
3.10、密码验证服务类
1、validate()方法用来验证用户密码。
2、它先从AuthenticationContextHolder中获取当前认证的用户名和密码。
3、然后调用matches()方法来校验密码。
4、matches()方法使用BCryptPasswordEncoder对存储的密文密码进行匹配验证。
5、如果匹配成功则验证成功,失败则验证失败。
这样通过Spring Security的AuthenticationContextHolder可以获取到当前认证principal的信息。
再结合密码加密匹配验证,就可以在服务中方便的实现密码的验证。
关键代码:
java
@Service
public class PasswordServiceImpl {
public void validate(User user)
{
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String AuthUsername = usernamePasswordAuthenticationToken.getName();
String AuthPassword = usernamePasswordAuthenticationToken.getCredentials().toString();
if (matches(user, AuthPassword)) {
System.out.println("验证成功!");
} else {
System.out.println("验证失败!");
}
}
public boolean matches(User user, String rawPassword)
{
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, user.getPassword());
}
}
3.11、认证用户服务类
1、实现Spring Security的UserDetailsService接口
UserDetailsService是Spring Security用于加载用户信息的核心接口。自定义实现可以灵活控制用户信息加载过程。
2、根据用户名加载用户信息通过userService查询数据库获取用户对象,包含用户信息如用户名、密码、角色等。
3、验证用户密码使用passwordService进行密码验证,校验登录的密码是否正确。
4、构建用户权限信息将用户的角色信息转换成GrantedAuthority授权信息集合。
5、封装用户对象返回将用户信息、权限信息封装到LoginUser对象中返回作为UserDetails。
6、在登录验证时提供用户详细信息Spring Security在登录验证时会调用此服务获取用户详细信息,以进行认证和授权。
关键代码:
java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private PasswordServiceImpl passwordService;
@Resource
private UserServiceImpl userService;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userService.selectUserByUsername(username);
passwordService.validate(user);
//取出角色和权限信息
Set<Role> roles = user.getRoles();
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new LoginUser(user,authorities);
}
}
3.12、登录接口
所有的登录都必须走这个接口,登录成功以后会返回给用户一个令牌,以访问系统受保护的资源。
关键代码:
java
@RestController
public class LoginController {
@Resource
private LoginServiceImpl loginService;
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
ajax.put(Constants.TOKEN, token);
return ajax;
}
}
3.13、登录接口核心逻辑
1、创建UsernamePasswordAuthenticationToken,包含用户名和密码。
2、将该authenticationToken设置到AuthenticationContextHolder,这是Spring Security提供的一个存储authentication的holder。
3、调用AuthenticationManager的authenticate方法进行认证。这个方法会根据配置调用相关的UserDetailsService等进行认证。(下面会走认证用户信息服务类中的loadUserByUsername方法),验证成功以后会返回一个authentication对象。
4、清空AuthenticationContextHolder。
5、从authentication对象中获取登录用户信息LoginUser。
6、使用TokenService生成JWT token。
7、返回JWT token。
关键代码:
java
@Service
public class LoginServiceImpl {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private TokenService tokenService;
public String login(String username, String password)
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
AuthenticationContextHolder.clearContext();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return tokenService.createToken(loginUser);
}
}
3.14、测试接口
用于验证已登录/未登录时访问的测试接口。
关键代码:
java
@RestController
public class HelloController {
@GetMapping("/hello")
private String hello(){
return "Hello World!";
}
}
3.15、总结
最后我们通过实际的项目来总结一下本次SpringSecurity的认证流程:
1、在LoginService的login方法中,构造一个UsernamePasswordAuthenticationToken,包含用户名和密码,这是开始认证的入口。
2、LoginService调用AuthenticationManager的authenticate方法启动认证流程。
3、AuthenticationManager会找到一个匹配的AuthenticationProvider来进行认证。
4、AuthenticationProvider会调用UserDetailsService的loadUserByUsername方法加载用户信息。这里我们通过UserDetailsServiceImpl来查询用户。
5、在UserDetailsServiceImpl中,根据用户名查询用户信息,然后调用PasswordService进行密码验证。
6、PasswordService通过AuthenticationContextHolder获取登录的用户名和密码。然后与数据库中存储的用户密码(经过编码)进行匹配,如果匹配上就验证成功。
7、PasswordService验证成功后,UserDetailsServiceImpl将根据用户信息构造一个UserDetails对象(这里是LoginUser),包含了用户名,密码,权限信息等。
8、UserDetailsServiceImpl将UserDetails返回给AuthenticationProvider。
9、AuthenticationProvider收到UserDetails后,完成验证,并生成一个已认证的Authentication对象。
10、AuthenticationProvider将Authentication返回给AuthenticationManager。
11、AuthenticationManager设置该Authentication到SecurityContextHolder中,供后续访问控制使用。
12、LoginService拿到已认证的Authentication,从中取出UserDetails,生成JWTtoken并返回。
综上,结合项目的逻辑SpringSecurity的认证流程大体可以分为:获取用户信息->用户验证->构建UserDetails->生成Authentication。我们通过自定义UserDetailsService和PasswordService来实现了用户验证逻辑。
四、运行项目
4.1、登录成功
通过post请求发送json格式的数据进行登录。
登录成功了并返回了Token令牌!
然后把刚才获得的令牌设置到请求头当中,进行访问。

可以看到对系统的受保护资源已经有了权限进行访问!
4.2、登录失败
上面登录成功了,下面我们故意输错密码,登录失败!

可以很明显的看到,系统返回了401。
如果我们尝试访问测试接口,因为没有令牌,我强行访问!

很明显,依旧提示权限不足!
五、Gitee源码地址
因为本篇博客提供的代码不是完整的,所以我把完整的项目开源到了码云上,供大家学习和参考!
项目地址:SpringBoot整合SpringSecurity+JWT+Redis完整教程
六、总结
以上就是我对于SpringSecurity以及如何在实际项目当中开发应用的个人理解,如有问题欢迎评论区留言!