SpringSecurity整合JWT
Spring-Security整合jwt
一、有状态登录和无状态登录
有状态:
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,不支持集群化部署
无状态:
微服务集群中的每个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
那么这种无状态性有哪些好处呢?
客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
服务端的集群和状态对客户端透明
服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
减小服务端存储压力
二、如何实现无状态登录
无状态登录的流程:
首先客户端发送账户名/密码到服务端进行认证
认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端
以后客户端每次发送请求,都需要携带认证的token
服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息
三、JWT简介
JWT包含三部分数据:
Header:头部,通常头部有两部分信息:
- 声明类型,这里是JWT
- 加密算法,自定义
我们会对头部进行Base64Url编码(可解码),得到第一部分数据。
Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:
- iss (issuer):表示签发人
- exp (expiration time):表示token过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
这部分也会采用Base64Url编码,得到第二部分数据。
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。
生成的数据格式如下:
eyJhbGciOiJIUzUxMiJ9.
==>
{"alg":"HS512"}
eyJhdXRob3JpdGllcyI6IlJPTEVfdXNlciwiLCJzdWIiOiJ6cyIsImV4cCI6MTU4NTY2ODQyM30.
==>
{"authorities":"ROLE_user,","sub":"zs","exp":1585668423}
5IgsesOWhkVPpyLbIHWbgeNNQFZvIRik2FlZHWCIoD7jH1b2BOgYHqWKb7LehZ_lyK53ZggXIdLVQJ_cqnVPtQ
最后一段无法解密
jwt交互流程:
- 应用程序或客户端向授权服务器请求授权
- 获取到授权后,授权服务器会向应用程序返回访问令牌
- 应用程序使用访问令牌来访问受保护资源(如API)
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。
四、基本配置
1、pom.xml
jjwt
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.lc</groupId>
<artifactId>springsecurity-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springsecurity-jwt</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、实体
package org.lc.springsecurityjwt.bean;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @BelongsProject: springsecurity-jwt
* @BelongsPackage: org.lc.springsecurityjwt.bean
* @Author: lc
* @CreateTime: 2020-03-31 16:53
* @Description:
*/
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
public User setId(Integer id) {
this.id = id;
return this;
}
public Integer getId() {
return id;
}
public User setUsername(String username) {
this.username = username;
return this;
}
public User setPassword(String password) {
this.password = password;
return this;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3、jwt登录的过滤器
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
// 从过滤的请求中 ,获得流。获取登录的用户对象
// 以json形式登录
User user=new ObjectMapper().readValue(httpServletRequest.getInputStream(), User.class);
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
}
//登录成功的回调
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 获取登录用户的角色
Collection<? extends GrantedAuthority> authorities=authResult.getAuthorities();
StringBuffer sb=new StringBuffer();
for (GrantedAuthority authority : authorities) {
// 将角色所有角色加到一个以逗号隔开的字符串中
sb.append(authority.getAuthority()).append(",");
}
// 构建jwt
String jwt = Jwts.builder()
// 构建角色
.claim("authorities", sb)
// 构建的主题 一般为用户名
.setSubject(authResult.getName())
// 设置过期时间为 当前时间加上一个小时,即一个小时的过期时间
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
// 签名
.signWith(SignatureAlgorithm.HS512, "louchen")
.compact();
Map<String,String> map=new HashMap<>();
map.put("token", jwt);
map.put("msg", "登录成功!");
response.setContentType("application/json;charset=utf-8");
PrintWriter printWriter=response.getWriter();
printWriter.write(new ObjectMapper().writeValueAsString(map));
printWriter.flush();
printWriter.close();
}
// 登录失败的回调
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
Map<String,String> map=new HashMap<>();
map.put("msg", "登录失败!");
response.setContentType("application/json;charset=utf-8");
PrintWriter printWriter=response.getWriter();
printWriter.write(new ObjectMapper().writeValueAsString(map));
printWriter.flush();
printWriter.close();
}
}
4、jwt的Token验证过滤器
public class JwtFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)servletRequest;
String jwtToken = req.getHeader("authorization");
Jws<Claims> jws= Jwts.parser().setSigningKey("louchen")
.parseClaimsJws(jwtToken.replace("Bearer", ""));
Claims claims=jws.getBody();
String username = claims.getSubject();
List<GrantedAuthority> authorities= AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(servletRequest, servletResponse);
}
}
5、security配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lc")
.password("$2a$10$qrWxqVUCyOc.C5/uTx.9cO8cu89ast7g.mGhSms8yfj/4VPGRLab2")
.roles("admin")
.and()
.withUser("zs")
.password("$2a$10$qrWxqVUCyOc.C5/uTx.9cO8cu89ast7g.mGhSms8yfj/4VPGRLab2")
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello")
.hasRole("user")
.antMatchers("/admin")
.hasRole("admin")
.antMatchers(HttpMethod.POST,"/login")
// 允许登录的接口
.permitAll()
.anyRequest().authenticated()
.and()
// 传入JwtLoginFilter中第一个参数 。作为过滤之前的请求
.addFilterBefore(new JwtLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
}
6、controller
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello everyone";
}
@GetMapping("/admin")
public String admin() {
return "hello admin";
}
}
7、请求测试
请求登录:
POST: http://localhost:8080/login
content-type: application/json
==> 请求参数
{
"username":"zs",
"password":"123"
}
结果==>
{
"msg": "登录成功!",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfdXNlciwiLCJzdWIiOiJ6cyIsImV4cCI6MTU4NTY2ODQyM30.5IgsesOWhkVPpyLbIHWbgeNNQFZvIRik2FlZHWCIoD7jH1b2BOgYHqWKb7LehZ_lyK53ZggXIdLVQJ_cqnVPtQ"
}
请求接口:
GET: http://localhost:8080/hello
选择Authorization的type为Bearer Token ,将Token输入。每次请求携带此token