欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot整合Jwt

程序员文章站 2022-05-23 08:37:16
...

jwt的简单概念

一、 JWT 是什么?

JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥**对进行签名。

简单来说,就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息。

优点:

1)生产的 token 可以包含基本信息,比如 id、用户昵称、头像等信息,避免再次查库

2)存储在客户端,不占用服务端的内存资源

缺点:

token 是经过 base64 编码,所以可以解码,因此 token 加密前的对象不应该包含敏感信息,如用户权限,密码等

二、JWT 格式组成:头部、负载、签名

header(可反解)+payload(可反解)+signature(不可反解)

头部:主要是描述签名算法

负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如 iss 签发者,exp 过期时间,sub 面向的用户

签名:主要是把前面两部分进行加密,防止别人拿到 token 进行 base 解密后篡改 token

三、关于jwt客户端存储

可以存储在 Cookie,localStorage 和 sessionStorage 里面

jwt和springboot整合的基本栗子

一、添加依赖

		<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>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

二、config配置

import com.example.springbootjwt.filter.JwtFilter;
import com.example.springbootjwt.filter.JwtLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/7/11 18:22
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.inMemoryAuthentication().withUser("admin")
                .password("$2a$10$Tg3qH41r.M53Bse1axlUBefSpsEc88br3L.3NDTr2PmXwKsaAiJ12")
                .roles("admin")
                .and()
                .withUser("leo")
                .password("$2a$10$Tg3qH41r.M53Bse1axlUBefSpsEc88br3L.3NDTr2PmXwKsaAiJ12")
                .roles("user");
    }
}

这些都是security最基本的配置,没什么好说的。在security5之后,必须要加密,所以加了个BCryptPasswordEncoder的Bean,AuthenticationManagerBuilder是授权,给予了admin和user两个角色,这里的password必须是加密过后的,所以你去测试类里面把BCryptPasswordEncoder注入进去,使用encode方法

JjwtApplicationTests.java

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootTest
class JjwtApplicationTests
{
    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;
    @Test
    void contextLoads()
    {
        System.out.println(bCryptPasswordEncoder.encode("123456"));
    }
}

三、写个控制层

HelloController.java

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/7/11 18:28
 */
@RestController
public class HelloController
{
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    @GetMapping("/admin")
    public String admin(){
        return "admin";
    }
}

然后打开Google,输入 http://localhost:8080/hello,他会重定向到login进行登录,然后输入已经授权的用户名和密码,当然这只是看一下神奇(个屁)的security框架,下面还需要继续定义配置类。

四、配置一个实体类

user.java

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/7/11 18:32
 */
@Data
public class User implements UserDetails
{
    private String username;
    private String password;
    private List<GrantedAuthority> authorities;//角色

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        return authorities;
    }

    @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;
    }
}

基本配置,没什么好说的

五、定义过滤器,创建个filter包

JwtLoginFilter.java

import com.example.springbootjwt.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/7/11 18:35
 */
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter
{
    private static final Log log= LogFactory.getLog(JwtLoginFilter.class);

    //这里要重写一个构造,参数是defaultFilterProcessesUrl,authenticationManager
    public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager)
    {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException
    {
        //认证方式以json形式进行验证
        User user = new ObjectMapper().readValue(req.getInputStream(),User.class);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        return getAuthenticationManager().authenticate(token);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException
    {
        log.info("authResult:"+authResult.toString());
        /**
         * authResult:org.springframewaaa@qq.com3b57d2cd:
         * Principal: aaa@qq.com:
         * Username: admin; Password: [PROTECTED];
         * Enabled: true; AccountNonExpired: true;
         * credentialsNonExpired: true;
         * AccountNonLocked: true;
         * Granted Authorities: ROLE_admin;
         * Credentials: [PROTECTED];
         * Authenticated: true;
         * Details: null;
         * Granted Authorities: ROLE_admin
         */
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
        StringBuffer buffer = new StringBuffer();
        for (GrantedAuthority authority : authorities)
        {
            buffer.append(authority.getAuthority()).append(",");
        }

        //authorities:ROLE_admin,
        log.info("authorities:"+buffer);
        String jwt = Jwts.builder()
                .claim("authorities", buffer)
                //getName这个方法,在Authentication的继承类里面Principal
                .setSubject(authResult.getName())
                //设置过期时间
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(SignatureAlgorithm.HS512, "leo123456")
                .compact();
        Map<String, String> map = new HashMap<>();
        map.put("token",jwt);
        map.put("msg","登陆成功");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(map));
        writer.flush();
        writer.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 writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(map));
        writer.flush();
        writer.close();
    }
}

这个配置首先 继承AbstractAuthenticationProcessingFilter,他重写的attemptAuthentication,它是把用户输入的用户名和密码获取到然后放到user的实体里面

UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),
然后生成的Authentication会被交由AuthenticationManager来进行管理
而AuthenticationManager管理一系列的AuthenticationProvider,
而每一个Provider都会通UserDetailsService和UserDetail来返回一个
以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication

然后重写两个方法,successfulAuthentication、unsuccessfulAuthentication,当然主要还是要看successfulAuthentication,看他的参数

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException

HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult

这里我想去看看authResult里面到底存放了些什么,不过我想可能存放了用户认证的一些基本信息

/**
         * authResult:org.springframewaaa@qq.com3b57d2cd:
         * Principal: aaa@qq.com:
         * Username: admin; Password: [PROTECTED];
         * Enabled: true; AccountNonExpired: true;
         * credentialsNonExpired: true;
         * AccountNonLocked: true;
         * Granted Authorities: ROLE_admin;
         * Credentials: [PROTECTED];
         * Authenticated: true;
         * Details: null;
         * Granted Authorities: ROLE_admin
         */

从打印的信息可以看到这里面有啥哦,Principal、Username用户名、Enabled是啥?不知道,我想大概是用户是否可用吧,credentialsNonExpired密码没有过期?AccountNonLocked账户没有被锁?等等等,大致就是你在user里面定义的那些东西。

生成jwt

String jwt = Jwts.builder()
                .claim("authorities", buffer)
                //getName这个方法,在Authentication的继承类里面Principal
                .setSubject(authResult.getName())
                //设置过期时间
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(SignatureAlgorithm.HS512, "leo123456")
                .compact();

把token放到map集合里面,然后把对象转成json字符串发给前端

JwtFilter.java

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/7/11 19:50
 */
public class JwtFilter extends GenericFilterBean
{
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
    {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String authorization = req.getHeader("authorization");
        Jws<Claims> jws = Jwts.parser().setSigningKey("leo123456").
                parseClaimsJws(authorization.replace("Bearer", ""));
        Claims body = jws.getBody();
        String username = body.getSubject();
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(((String) body.get("authorities")));

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, "", authorities);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

六、完善配置类

SecurityConfig.java

import com.example.springbootjwt.filter.JwtFilter;
import com.example.springbootjwt.filter.JwtLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/7/11 18:22
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.inMemoryAuthentication().withUser("admin")
                .password("$2a$10$Tg3qH41r.M53Bse1axlUBefSpsEc88br3L.3NDTr2PmXwKsaAiJ12")
                .roles("admin")
                .and()
                .withUser("leo")
                .password("$2a$10$Tg3qH41r.M53Bse1axlUBefSpsEc88br3L.3NDTr2PmXwKsaAiJ12")
                .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().addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),
                UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class)
        .csrf().disable();
    }
}

重写HttpSecurity的configure,关键是两个过滤器存放进来addFilterBefore把JwtLoginFilter和JwtFilter放进来

七、测试

使用postman,先登录
SpringBoot整合Jwt
然后会得到

{
    "msg": "登陆成功",
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE1OTQ0ODQ4NzF9.uKO8JJWT7EzDWJblV7UY1Oo9sHzfEDG3r1cZ2sTsNJHMixip-kfoGWyDj04OhM3UZ_Hv-8b1GeXX3Zib_PIcOA"
}

然后进入 http://localhost:8080/admin
SpringBoot整合Jwt
SpringBoot整合Jwt

总结

SpringBoot 整合SpringSecurity给我们的感觉就是很重,配置起来非常复杂,要记得地方很多。加上jwt我觉得也一样,配置起来比较复杂,如果不能系统的全面的了解这一套原理和流程,是很难理解和记忆的。而且这个案例是非常基本的,能大致的看出来jwt的基本的配置。

我在网上看过很多springboot整合jwt的案例,虽然各不相同但是大差不差,主要还是想去熟悉一下这些基本的流程,为后面整合OAuth2做一些准备。