SpringSecurity认证与授权(以限制登录访问视图为例)
以下类名尽量做到见名知意,如Congif的类一般放到config文件夹下,类似将不再赘述
目标
"/api/auth/login",
"/api/auth/register",
"/api/auth/sendCode"
这三个接口无需身份认证(因为依赖这三个接口登录,发放jwtToken)
其余所有接口皆使用jwt认证。否则返回401
1.添加Maven依赖坐标
<!--添加 Spring Security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.配置application.yaml
关闭自带的传统用户名和密码认证系统
spring:
security:
user:
name: disabled
password: disabled
在本文中,认证是通过自己编写的接口与jwt实现认证,没有用到它自带的认证系统。所以关闭。否则自带的认证系统会干扰我自身的认证逻辑
3.配置config
@Configuration
// 排除 Spring Security 默认的用户自动配置,避免创建 InMemoryUserDetailsManager// 这是因为我们使用 JWT 无状态认证,不需要默认的内存用户
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
//编写构造方法,让Spring识别到需要JwtAuthenticationFilter对象时自动注入进来
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter
){
this.jwtAuthenticationFilter =
jwtAuthenticationFilter;
}
/*这是一个Spring Security Filter Chain(Spring Security 过滤器链)
* 它通过 Servlet Filter 机制,在请求到达 Controller 之前拦截 HTTP 请求,然后判断请求是否允许继续执行。*/
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception {
//关闭csrf
http.csrf(AbstractHttpConfigurer::disable)
//JWT是无状态认证,不需要Session保存用户。关闭session
.sessionManagement(session ->
session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
//配置接口权限
.authorizeHttpRequests(
auth -> auth
//登录注册不需要JWT
//匹配相应的http请求
.requestMatchers(
"/api/auth/login",
"/api/auth/register",
"/api/auth/sendCode"
)
// 允许所有人访问
.permitAll()
//其他接口需要认证
.anyRequest() // 前面没有匹配到的所有请求。
.authenticated() // 要求当前请求已经通过身份认证。
)
//添加JWT过滤器
.addFilterBefore(
// 让jwtAuthenticationFilter这个自定义过滤器在UsernamePasswordAuthenticationFilter之前执行
jwtAuthenticationFilter,
// 它主要用于处理传统的:用户名+密码的登录方式
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
4.配置过滤器
@Slf4j
@Component
public class JwtAuthenticationFilter
extends OncePerRequestFilter {
// 此处是为了使用jwt相关功能
private final JwtUtil jwtUtil;
// 此处是为了使用redis相关功能,用Redis做token黑名单
@Autowired
RedisStringServer redisStringServer;
// 编写构造方法,构造器注入。为了让Spring自动注入JwtUtil。可以替换为Autowired,但构造器注入的好处是final 保证依赖不会被修改
public JwtAuthenticationFilter(
JwtUtil jwtUtil
){
this.jwtUtil = jwtUtil;
}
// 该方法的作用是实现授权功能
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// 获取Authorization请求头,其值中包含Jwt的token
String header = request.getHeader("Authorization");
// 判断jwtToken的合法性
if(header != null
&& header.startsWith("Bearer ")
&& SecurityContextHolder //true:当前请求还没有建立认证身份,可以继续执行 JWT 认证。false:当前请求已经有认证信息了,不需要再次通过 JWT 认证。
.getContext()
.getAuthentication() == null) {
String token = header.substring(7);
// 判断是否在黑名单中
boolean inBlackList = redisStringServer.existKey("jwt:blacklist:"+token);
if (inBlackList){
// 设置返回值为401
response.setStatus(
HttpServletResponse.SC_UNAUTHORIZED
);
//如果在黑名单中则直接返回。无需解析
return;
}
try {
//解析JWT
String email = jwtUtil.parse(token);
//创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
email,//标识用户是谁
null,//用户凭证,一般设为null
Collections.emptyList()//为该用户授予的角色。因为该文章的功能不需要分角色,所以传入了空的列表
);
//保存用户信息
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
// 日志记录当前状态(非必须添加)
log.info(
"认证状态:" + authentication.isAuthenticated()
);
log.info(
"当前用户:" + authentication.getPrincipal()
);
log.info(
"当前权限:" + authentication.getAuthorities()
);
}
catch(Exception e){
log.error("Jwt鉴权报错:",e);
//设置返回值
response.setStatus(
HttpServletResponse.SC_UNAUTHORIZED
);
return;
}
}
// 继续执行请求
// 把当前请求继续交给过滤器链中后面的过滤器处理。
filterChain.doFilter(
request,
response
);
}
}
至此,已经基本实现了认证和授权的基本操作。下面在附录中给出用到的工具类作为补充。但不是作为主要逻辑部分
附录
jwtUtil.java
@Slf4j
@Component
public class JwtUtil {
// 设置秘钥
@Value("${jwt.secret}")
private String SECRET;
@Value("${jwt.expiration}")
private int EXPRIATION;
// 转换秘钥类型
private SecretKey SECRET_KEY;
//使用此注解可以使SpringBoot在Bean创建完成后执行该方法
@PostConstruct
private void init(){
SECRET_KEY = Keys.hmacShaKeyFor(
SECRET.getBytes(StandardCharsets.UTF_8)
);
}
/**
* 根据用户邮箱 ,生成唯一token
* @param user_email 用户邮箱
* @return 类型为String的token
*/ public String generate(String user_email){
log.info("生成jwt,用户邮箱为:"+user_email);
String token = Jwts.builder()
//设置主题,一般以主键作为主题,因为唯一
.subject(user_email)
//设置过期时间
.expiration(new Date(
System.currentTimeMillis()+EXPRIATION))
// 将Jwt签名。因为Jwt不是加密,而是签名
.signWith(SECRET_KEY)
// 编码生成最终数据
.compact();
System.out.println(token);
return token;
}
/**
* 验证JwtToken
* @param token 前端保存的token
* @return 根据token解析出来的用户邮箱(String)
*/ public String parse(String token){
// 使用 JWT 解析器解析传入的 token // token 一般是前端请求携带的 Authorization: Bearer xxx 中的 xxx 部分
Claims claims =
Jwts.parser()
// 设置 JWT 验证使用的密钥
// 解析 JWT 时,会使用这个 SECRET_KEY 对 token 的签名部分进行校验
// 如果签名不正确,说明 token 被篡改,会抛出异常
.verifyWith(SECRET_KEY)
// 构建 JWT 解析器对象
// 此时解析器已经知道如何验证 token 的签名
.build()
// 解析签名后的 JWT // 会执行:
// 1. Base64 解码 Header // 2. Base64 解码 Payload // 3. 使用 SECRET_KEY 验证 Signature // 4. 检查 token 是否过期
.parseSignedClaims(token)
// 获取 JWT 的 Payload 部分
// 返回的是 Claims 对象,本质上就是一个 Map // 里面保存了 subject、expiration、自定义字段等信息
.getPayload();
// 获取 Payload 中的 subject 字段
// 你生成 JWT 时:
// .subject(user.getUser_email())
// // 所以这里获取的是用户邮箱
// 例如:
// {
// "sub": "12345678@qq.com", // "exp": 1750000000 // } // // 返回:
// "12345678@qq.com"
String subject = claims.getSubject();
System.out.println("验证后得到的主题:"+subject);
return subject;
}
/**
* 根据token得到token的过期时间
* @param token jwtToken
* @return 过期时间的毫秒时间戳。单位是毫秒
*/
public long getExpiration(String token) {
return Jwts.parser()
.verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.getTime();
}
}
RedisStringServer.java
@Service
public class RedisStringServer implements RedisStringServerImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void set(String key, String value, Duration ttl) {
stringRedisTemplate.opsForValue().set(key, value,ttl);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public void del(String key) {
stringRedisTemplate.delete(key);
}
@Override
public boolean existKey(String key) {
return stringRedisTemplate.hasKey(key);
}
}z

浙公网安备 33010602011771号