SpringBoot-SpringSecurity-Jwt的整合案例分析
一、Jwt概述
1、什么是Jwt?
- Jwt是Json Web Token的简称,它的声明一般用于在身份提供者和服务提供者之间传递被认证的身份信息,以便从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,改token也可直接被用于认证,也可以被加密。
2、Jwt的组成部分:header、payload、secret
- header(头部):存放加密算法,比如RS256非对称加密算法、HS256验签算法
- payload(载荷):携带存放的数据,比如用户名称、用户头像、权限信息等,在payload中一些标签的含义如下
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 - secret(密钥/盐值):secret主要存放在服务器端,主要作用是通过对Base64(header,payload)+secret进行加密得到签名值
3、Jwt的优缺点:
- 优点:
①一旦生成无需再向服务器查询存放的数据,减去服务器的压力
②轻量级,json风格比较简单
③跨语言 - 缺点:
①token一旦生成便无法修改
②无法更新token的有效期
③即使进行注销,清除cookie中的数据也无法销毁一个token
二、Jwt的具体使用api
1、Maven项目中引入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
2、创建Jwt
public class CreateJwt {
// 签名key 密钥 盐值
private static String SIGN_KEY = "SignKey";
public static void main(String[] args) {
// 创建jwt
JwtBuilder builder = Jwts.builder();
// 给jwt中添加payload信息
builder.claim("phone","1592****100");
// 给jwt设置header以及签名值
builder.signWith(SignatureAlgorithm.HS256,SIGN_KEY);
// 打印jwt
System.out.println(builder.compact());
}
}
3、解析Jwt
public class ParseJwt {
private static final String SING_KEY = "SignKey";
public static void main(String[] args) {
// 待解密jwt
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJwaG9uZSI6IjE1OTIqKioqMTAwIn0.L1IPdIDx5ANbmyRrVVyGjNjM6r4s5kAneU9RZB5UXFE";
// 解密的具体api实现
Claims body = Jwts.parser().setSigningKey(SING_KEY).parseClaimsJws(jwt).getBody();
// 打印解密结果得到payload
System.out.println(body);
}
}
4、给Jwt设置有效时间
public class SetJwtTimes {
private static final String SIGN_KEY = "SignKey";
public static void main(String[] args) {
// 获取当前时间
long now = System.currentTimeMillis();
// 过期时间为1分钟
long exp = now + 1000 * 1;
JwtBuilder jwt = Jwts.builder()
.setIssuedAt(new Date()) // 设置jwt刚刚创建的初始时间
.claim("userId", "1234")
.signWith(SignatureAlgorithm.HS256, SIGN_KEY)
.setExpiration(new Date(exp));// 设置jwt的有效时间
System.out.println(jwt.compact()); // 打印jwt的内容
// 模拟jwt过期,token失效
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
// 验签
Claims body = Jwts.parser().setSigningKey(SIGN_KEY).parseClaimsJws(jwt.compact()).getBody();
// 打印payload
System.out.println(body);
// {iat=1621523859, userId=1234, exp=1621523864}
// iat:代表jwt初始化的时间 exp:代表jwt失效时的时间
// iat和exp二者的差值为设置的jwt有效时间
}
}
5、手写Java代码简单实现Jwt(未实现有效期的设置)
底层原理分析:
- Jwt底层利用Base64编码器对header以及payload部分进行编码(注意Base64是编码器,所以这里对header和payload进行的仅为编码,而不是加密,仍然可以通过解码器对他们进行解码,所以他们仍然是明文);接着对{payload+SIGN_KEY(盐值)}进行md5加密,便得到sign签名值。最终通过字符串拼接出headerEncoded + “.” + payloadEncoded + “.” + sign,从而得到Jwt
注意:这里的SIGN_KEY(盐值/密钥)是自己声明定义的,存放在服务器端,防止Jwt被hk暴力破解并更改
不多说,上代码实现
public class JwtByHand {
// 声明一个盐值,防止jwt被hk暴力破解
private static final String SIGN_KEY = "sign";
public static void main(String[] args) {
// 手写jwt,封装三个部分 header、payload、sign签名值
// 我们要通过base64编码器对header以及payload进行一个base64的编码
// sign签名值需要对起进行md5加密,方便对payload中的数据进行比对,防止黑客通过抓包对payload中的数据进行篡改
// 定义header
JSONObject header = new JSONObject();
header.put("alg","HS256");
String headerEncoded = Base64.getEncoder().encodeToString(header.toJSONString().getBytes());
// 定义payload
JSONObject payload = new JSONObject();
payload.put("phone","1592****100");
String payloadEncoded = Base64.getEncoder().encodeToString(payload.toJSONString().getBytes());
// 定义sign签名值
String payloadStr = payload.toJSONString();
String sign = DigestUtils.md5DigestAsHex((payloadStr + SIGN_KEY).getBytes());
String jwt = headerEncoded + "." + payloadEncoded + "." + sign;
System.out.println(jwt);
// 解密jwt,也就是验签
String[] jwtSplit = jwt.split("\\.");
String payloadDecode = new String(Base64.getDecoder().decode(jwtSplit[1].getBytes()));
String signDecode = DigestUtils.md5DigestAsHex((payloadDecode + SIGN_KEY).getBytes());
// 打印,看是否验签成功
System.out.println(signDecode.equals(sign));
}
三、SpringSecurity和Jwt是如何整合的?
环境配置不再赘述
1、需要的实体类UserEntity
// 用户信息表
@Data // 使用lombok自动生成set、get方法。这里不再赘述
public class UserEntity implements UserDetails {
private Integer id;
private String username;
private String realname;
private String password;
private Date createDate;
private Date lastLoginTime;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
// 用户所有权限
private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
}
2、准备Jwt工具类
public class JwtUtils {
public static final String TOKEN_HEADER = "token";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SUBJECT = "subject";
// jwt的有效期
private static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;
// 签名中的盐值
private static final String APPSECRET_KEY = "sign_secret";
private static final String ROLE_CLAIMS = "roles";
public static String generateJsonWebToken(UserEntity user) {
String token = Jwts
.builder()
.setSubject(SUBJECT)
.claim(ROLE_CLAIMS, user.getAuthorities())
.claim("username", user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
/**
* 生成token
*
* @param username
* @param role
* @return
*/
public static String createToken(String username, String role) {
Map<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
String token = Jwts
.builder()
.setSubject(username)
.setClaims(map)
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
/**
* 解密、验签
* @param token
* @return
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取用户名
*
* @param token
* @return
*/
public static String getUsername(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
/**
* 获取用户角色
*
* @param token
* @return
*/
public static List<SimpleGrantedAuthority> getUserRole(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
List roles = (List) claims.get("role");
String json = JSONArray.toJSONString(roles);
List<SimpleGrantedAuthority>
grantedAuthorityList =
JSONArray.parseArray(json, SimpleGrantedAuthority.class);
return grantedAuthorityList;
}
/**
* 是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.getExpiration().before(new Date());
}
}
3、声明两个SpringSecurity的filter过滤器,来做“登陆成功的给请求头加入token”以及"请求过滤,验证权限"
- 声明JWTLoginFilter来做“登陆成功的给请求头加入token”
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
/**
* 获取授权管理
*/
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
/**
* 后端登陆接口
*/
super.setFilterProcessesUrl("/auth/login");
}
// 进入UserDetailsService之前会先尝试认证
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) {
try {
UserEntity user = new ObjectMapper()
.readValue(req.getInputStream(), UserEntity.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
logger.error(e.getMessage());
return null;
}
}
/**
* 账户密码验证成功,给请求头绑定jwt
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 获取主体
UserEntity userEntity = (UserEntity) authResult.getPrincipal();
// 给请求头绑定jwt
response.addHeader("token", MayiktJwtUtils.generateJsonWebToken(userEntity));
}
/**
* 账户密码验证失败
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.getWriter().print("账号或者密码错误");
}
}
- 声明JWTValidationFilter来做“请求过滤,验证权限”
public class JWTValidationFilter extends BasicAuthenticationFilter {
public JWTValidationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 请求过滤,过滤的目的是为了验证权限
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(setAuthentication(request.getHeader("token")));
super.doFilterInternal(request, response, chain);
}
/**
* 验证token 并且验证权限
* @param token
* @return
*/
private UsernamePasswordAuthenticationToken setAuthentication(String token) {
String username = JwtUtils.getUsername(token);
if (username == null) {
return null;
}
List<SimpleGrantedAuthority> userRoleList = JwtUtils.getUserRole(token);
return new UsernamePasswordAuthenticationToken(username, null, userRoleList);
}
}
4、UserServiceDetails实现类
@Service
public class MemberUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.根据username查询用户是否存在
UserEntity user = userMapper.findByUsername(username);
// 如果用户不存在
if(user == null){
return null;
}
// 2.如果用户存在,则获取用户的权限
List<PermissionEntity> permission = userMapper.findPermissionByUsername(username);
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
for (PermissionEntity permissionEntity : permission){
authorities.add(new SimpleGrantedAuthority(permissionEntity.getPermTag()));
}
// 3.将该权限添加到security中
user.setAuthorities(authorities);
return user;
}
}
4、对自定义SpringSecurity配置类
@Component // 向容器中注入组件
@EnableWebSecurity // 开启web模块下的安全验证功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MemberUserDetailsService memberUserDetailsService;
@Autowired
private PermissionMapper permissionMapper;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberUserDetailsService).passwordEncoder(new PasswordEncoder() {
// MD5Util是自定义的md5加密工具类,这里不再赘述
@Override
public String encode(CharSequence charSequence) {
return MD5Util.encode((String) charSequence);
}
/*
charSequence:用户输入的密码
encodedPswd:从数据库db中查询到的密码
*/
@Override
public boolean matches(CharSequence charSequence, String encodedPswd) {
String encode = MD5Util.encode((String) charSequence);
return encodedPswd.equals(encode);
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
List<PermissionEntity> allPermission = permissionMapper.findAllPermission();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.authorizeRequests();
for (PermissionEntity permissionEntity : allPermission){
expressionInterceptUrlRegistry.antMatchers(permissionEntity.getUrl()).hasAuthority(permissionEntity.getPermTag());
}
expressionInterceptUrlRegistry.antMatchers("/auth/login").permitAll().antMatchers("/**")
.fullyAuthenticated().and()
.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTValidationFilter(authenticationManager()))
.csrf()
.disable()
// 使用了jwt之后,我们就要关闭自动创建session,不再用session来保存用户信息
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
四、Jwt的注销问题
- 清除浏览器cookie(但是服务器还是存在,并不是真正意义上的注销)
- 建议将Jwt的有效期设置短一些