SpringBoot整合Jwt
目录
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,先登录
然后会得到
{
"msg": "登陆成功",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE1OTQ0ODQ4NzF9.uKO8JJWT7EzDWJblV7UY1Oo9sHzfEDG3r1cZ2sTsNJHMixip-kfoGWyDj04OhM3UZ_Hv-8b1GeXX3Zib_PIcOA"
}
然后进入 http://localhost:8080/admin
总结
SpringBoot 整合SpringSecurity给我们的感觉就是很重,配置起来非常复杂,要记得地方很多。加上jwt我觉得也一样,配置起来比较复杂,如果不能系统的全面的了解这一套原理和流程,是很难理解和记忆的。而且这个案例是非常基本的,能大致的看出来jwt的基本的配置。
我在网上看过很多springboot整合jwt的案例,虽然各不相同但是大差不差,主要还是想去熟悉一下这些基本的流程,为后面整合OAuth2做一些准备。