SpringSecurity整合JWT

Lou.Chen
大约 6 分钟

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交互流程:
  1. 应用程序或客户端向授权服务器请求授权
  2. 获取到授权后,授权服务器会向应用程序返回访问令牌
  3. 应用程序使用访问令牌来访问受保护资源(如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/logincontent-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