1. 背景
在做通用权限系统的时候,我使用了spring-security来控制权限系统,现在就最基本的使用做个总结
2. 使用demo
2.1 一些基本的概念
Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)
Spring Security登录认证主要涉及两个重要的接口 UserDetailService和UserDetails接口。 UserDetailService接口主要定义了一个方法 loadUserByUsername(String username)用于完成用户信息的查询,其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接 口,完成数据库查询,该接口返回UserDetail。
loadUserByUsername 用户返回的是UserDetails,我们自己的User实现UserDetails
UserDetail主要用于封装认证成功时的用户信息,即UserDetailService返回的用户信息,可以用Spring 自己的User对象,但是最好是实现UserDetail接口,自定义用户对象
认证成功返回token是什么
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户 名和密码
基本的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 Spring Security认证步骤
-
自定UserDetails的实现类User:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义 UserDetails
-
自定义UserDetailsService类,主要用于从数据库查询用户信息。
将User类实现UserDetails接口
将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修改成boolean类型,同时添加authorities属性、permisssion属性
-
创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。
LoginSuccessHandler implements AuthenticationSuccessHandler
实现onAuthenticationSuccess方法
生成token、创建登录结果对象、获取输出流、把生成的token存到redis
-
创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。
LoginFailureHandler implements AuthenticationFailureHandler,重写onAuthenticationFailure()方法
String result = JSON.toJSONString(Result.error().code(code).message(message)); outputStream.write(result.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); outputStream.close(); -
创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON。
CustomerAccessDeniedHandler implements AccessDeniedHandler
-
创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON。
AnonymousAuthenticationHandler implements AuthenticationEntryPoint
-
配置Spring Security配置类,把上面自定义的处理器交给Spring Security。
3. JWT 的原理和使用
典型的时间换空间的设计模式
3.1 JWT的优势
-
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
-
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
-
因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
-
不需要在服务端保存会话信息,特别适用于分布式微服务。
3.2.令牌组成
-
1.标头(Header)
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分
-
2.有效载荷(Payload)
-
3.签名(Signature)
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
3.3 JWT 加密解密的全过程
@Test
void contextLoads() {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,100);
String token = JWT.create()
.withClaim("userid",12)
.withClaim("username", "xiaochen")//payload
.withExpiresAt(instance.getTime())//指定令牌过期时间
.sign(Algorithm.HMAC256("!Q@W#E$R"));//签名
System.out.println(token);
}
@Test
public void test(){
//创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!Q@W#E$R")).build();
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTYxMjg1MjMsInVzZXJpZCI6MTIsInVzZXJuYW1lIjoieGlhb2NoZW4ifQ.WhsEsCcf6c4hKr1eyhy6psQsaVIPr3Ibfqo3vM8sHKA\n");
System.out.println(verify.getClaim("userid").asInt());
System.out.println(verify.getClaim("username").asString());
System.out.println("过期时间: "+verify.getExpiresAt());
}
3.4 JWT 常见异常信息
-
SignatureVerificationException:签名不一致异常
-
TokenExpiredException:令牌过期异常
-
AlgorithmMismatchException:算法不匹配异常
-
InvalidClaimException: 失效的payload异常
3.5 JWT 封装工具类
public class JWTUtils {
private static final String SIGN = "!Q@W3e4r%T^Y";
/**
* 生成token header.payload.sign
*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);
JWTCreator.Builder builder = JWT.create();
map.forEach(builder::withClaim);
return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SIGN));
}
/**
* 验证token 合法性, 不抛异常就是验证通过
* 获取token中payload
*
*/
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
}