文章总结自三更草堂SpringSecurity框架教程,个人认为是B站最好用的Security+JWT讲解。
SpringBoot使用的是2.7.0版本
依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
创建一个HelloController
:
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String hello(){
return "hello";
}
}
访问测试:可以看到可以访问成功
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
重启项目之后发现localhost:8088/hello
接口无法再次进行访问,取而代之的是Security的登录页。
本质使用的是过滤器链,内部提供各种功能的过滤器。
UsernamePasswordAuthenticationFilter
:处理登录页中的登录请求;ExceptionTranslationFilter
:处理过滤器链中的异常;FilterSecurityInterceptor
:权限校验过滤器。Authentication
:实现类,表示当前访问系统的用户,封装了相关用户信息;AuthenticationManager
:定义认证Authentication
方法;UserDetailsService
:加载用户特定数据的核心接口;包含一个根据用户名查询用户信息的方法。UserDetails
:提供核心用户信息,通过UserDetailsService
根据用户名获取处理的用户信息封装成UserDetailsService
对象返回,然后将信息封装到Authentication
对象中。使用自己定义的接口去掉哟SpringSecurity中封装的类。
ProviderManager
的方法进行认证;认证通过生成JWT;将用户信息存入redis;UserDetailsService
:实现数据库查询。SecurityContextHolder
。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.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
dependency>
@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"));
/**
* 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工具类来实现加密和解密。
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public Result login(@RequestBody SysUser user){
return new Result(200,"登陆成功",loginService.login(user));
}
}
@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();
}
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);
}
}
@Data
@AllArgsConstructor
public class TokenVO {
private String token;
}
@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);
}
}
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);
}
@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();
}