文章总结自三更草堂SpringSecurity框架教程,个人认为是B站最好用的Security+JWT讲解。
1. 项目搭建
1.1 新建SpringBoot项目
SpringBoot使用的是2.7.0版本
依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
创建一个HelloController
:
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String hello(){
return "hello";
}
}
访问测试:可以看到可以访问成功
1.2 引入SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
重启项目之后发现localhost:8088/hello
接口无法再次进行访问,取而代之的是Security的登录页。
此时的默认用户名是user
,密码会在控制台输出。
2. 认证
2.1 登录流程
2.2 security认证流程
本质使用的是过滤器链,内部提供各种功能的过滤器。
-
核心过滤器:
UsernamePasswordAuthenticationFilter
:处理登录页中的登录请求;ExceptionTranslationFilter
:处理过滤器链中的异常;FilterSecurityInterceptor
:权限校验过滤器。
-
1.2案例认证流程:
Authentication
:实现类,表示当前访问系统的用户,封装了相关用户信息;AuthenticationManager
:定义认证Authentication
方法;UserDetailsService
:加载用户特定数据的核心接口;包含一个根据用户名查询用户信息的方法。UserDetails
:提供核心用户信息,通过UserDetailsService
根据用户名获取处理的用户信息封装成UserDetailsService
对象返回,然后将信息封装到Authentication
对象中。
2.3 实现思路
使用自己定义的接口去掉哟SpringSecurity中封装的类。
-
登录流程:
-
校验过程:
-
引入redis:如果认证之后害需要通过JWT中的
userid
对数据库进行查询,消耗太大,可以存储入到redis中。
2.4 实现方案
- 登录:
- 自定义登录接口:调用
ProviderManager
的方法进行认证;认证通过生成JWT;将用户信息存入redis; - 自定义
UserDetailsService
:实现数据库查询。
- 自定义登录接口:调用
- 校验:
- 定义JWT过滤器:获取token,解析token,获取userid,去redis中获取用户信息;将用户信息存入
SecurityContextHolder
。
- 定义JWT过滤器:获取token,解析token,获取userid,去redis中获取用户信息;将用户信息存入
2.5 简单实现
- 创建数据库:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements Serializable {
@TableId
private Long id;
private String username;
private String password;
private Character status;
private Integer delFlag;
}
- 引入数据库相关依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
- 配置mapper
@Component
public interface SysUserMapper extends BaseMapper<SysUser> {
}
添加注解扫描
@SpringBootApplication
@MapperScan("com.jm.springsecurity.mapper")
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
- 构建登录接口:实现
security
中的类,自己实现登录逻辑UserDetailsService
@Service
public class SysUserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
SysUser user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
return new LoginUser(user);
}
}
- 封装登录返回实体类:实现
UserDetails
接口自定义返回逻辑。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@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;
}
}
-
登录测试: 登录失败,原因是
security
拥有自己的密码校验,需要重写。
新建
SecurityConfig
将BCryptPasswordEncoder
注入到spring容器中,security就会自动使用来替换掉原有的密码校验方式。@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
加密:针对密码进行加密之后,将加密后的密码存入数据库,登录时会自动走当前加密方式,不需要再做多余处理,即可登录成功。
@Autowired private PasswordEncoder passwordEncoder;
System.out.println(passwordEncoder.encode("1234"));
2.6 JWT使用
- 工具类
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "jmyy";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
// 加密
String jwt = createJWT("2123");
System.out.println(jwt);
// 解密
Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMzU5Yzk3ZGFmNDE0MzEzOTMyZjYxMDZkNWYyNzg4YSIsInN1YiI6IjIxMjMiLCJpc3MiOiJzZyIsImlhdCI6MTY1NDQ5ODM4MCwiZXhwIjoxNjU0NTAxOTgwfQ.hAFpmr6u_AtlMEs9SqS9TT9yuzbdSDDNsuWMLWmKIgU");
String subject = claims.getSubject();
System.out.println(subject);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
使用JWT工具类来实现加密和解密。
2.7 自定义登录接口
- 新增接口:
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public Result login(@RequestBody SysUser user){
return new Result(200,"登陆成功",loginService.login(user));
}
}
- Security放行登录接口:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
- 配置redis:
spring:
redis:
host: localhost
port: 6379
database: 0
password: 123456
-
认证逻辑:
- 将
AuthenticationManager
注入到容器中
@Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); }
- 登录接口实现:
/** * @Description * @date 2022/6/6 15:02 */ public interface LoginService { TokenVO login(SysUser user); }
@Service public class LoginServiceImpl implements LoginService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public TokenVO login(SysUser user) { // 认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 认证失败 if (Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } // 认证成功 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); // 根据用户id生成jwt String jwt = JwtUtil.createJWT(userId); // 将用户信息存入redis redisCache.setCacheObject("login:"+userId,loginUser); return new TokenVO(jwt); } }
- 返回vo
@Data @AllArgsConstructor public class TokenVO { private String token; }
- 将
2.8 jwt过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (StringUtils.hasText(token)) {
//直接放行 让后面原生的 security 去拦截
filterChain.doFilter(request, response);
return;
}
// 解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token error");
}
// 从redis 获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户为登录");
}
// 将用户信息存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
loginUser,
null,
null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
- 配置jwt过滤器:将当前过滤器放到
security
的UsernamePasswordAuthenticationFilter
前面。修改SecurityConfig
类
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// .antMatchers("/testCors").hasAuthority("system:dept:list222")
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加过滤器到某个过滤器前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
2.9 退出登录
@Override
public Result logout() {
// 获取SecurityContextHolder中用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
// 删除redis中值
redisCache.deleteObject("login:"+userId);
return new Result(200,"注销成功");
}
@GetMapping("/logout")
public Result logout(){
return loginService.logout();
}
相关文章
暂无评论...