用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的。现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能够访问API。
Git地址:https://github.com/X-rapido/jwt-spring-boot-restful-api
一、初探JWT
1、什么是JWT
JWT(Json Web Token),是一种工具,格式为XXXX.XXXX.XXXX的字符串,JWT以一种安全的方式在用户和服务器之间传递存放在JWT中的不敏感信息。
2、为什么要用JWT
设想这样一个场景,在我们登录一个网站之后,再把网页或者浏览器关闭,下一次打开网页的时候可能显示的还是登录的状态,不需要再次进行登录操作,通过JWT就可以实现这样一个用户认证的功能。当然使用Session可以实现这个功能,但是使用Session的同时也会增加服务器的存储压力,而JWT是将存储的压力分布到各个客户端机器上,从而减轻服务器的压力。
3、JWT长什么样
JWT由3个子字符串组成,分别为Header,Payload以及Signature,结合JWT的格式即:Header.Payload.Signature。(Claim是描述Json的信息的一个Json,将Claim转码之后生成Payload)。
Header
Header是由以下这个格式的Json通过Base64编码(编码不是加密,是可以通过反编码的方式获取到这个原来的Json,所以JWT中存放的一般是不敏感的信息)生成的字符串,Header中存放的内容是说明编码对象是一个JWT以及使用“SHA-256”的算法进行加密(加密用于生成Signature)
{
"typ":"JWT",
"alg":"HS256"
}Claim
Claim是一个Json,Claim中存放的内容是JWT自身的标准属性,所有的标准属性都是可选的,可以自行添加,比如:JWT的签发者、JWT的接收者、JWT的持续时间等;同时Claim中也可以存放一些自定义的属性,这个自定义的属性就是在用户认证中用于标明用户身份的一个属性,比如用户存放在数据库中的id,为了安全起见,一般不会将用户名及密码这类敏感的信息存放在Claim中。将Claim通过Base64转码之后生成的一串字符串称作Payload。
{
"iss":"Issuer —— 用于说明该JWT是由谁签发的",
"sub":"Subject —— 用于说明该JWT面向的对象",
"aud":"Audience —— 用于说明该JWT发送给的用户",
"exp":"Expiration Time —— 数字类型,说明该JWT过期的时间",
"nbf":"Not Before —— 数字类型,说明在该时间之前JWT不能被接受与处理",
"iat":"Issued At —— 数字类型,说明该JWT何时被签发",
"jti":"JWT ID —— 说明标明JWT的唯一ID",
"user-definde1":"自定义属性举例",
"user-definde2":"自定义属性举例"
}Signature
Signature是由Header和Payload组合而成,将Header和Claim这两个Json分别使用Base64方式进行编码,生成字符串Header和Payload,然后将Header和Payload以Header.Payload的格式组合在一起形成一个字符串,然后使用上面定义好的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,这个字符串就是Signature。如图所示
4、JWT实现认证的原理
服务器在生成一个JWT之后会将这个JWT会以Authorization : Bearer JWT 键值对的形式存放在cookies里面发送到客户端机器,在客户端再次访问收到JWT保护的资源URL链接的时候,服务器会获取到cookies中存放的JWT信息,首先将Header进行反编码获取到加密的算法,在通过存放在服务器上的密匙对Header.Payload 这个字符串进行加密,比对JWT中的Signature和实际加密出来的结果是否一致,如果一致那么说明该JWT是合法有效的,认证成功,否则认证失败。
二、JWT实现用户认证的流程图
三、JWT的代码实现
框架介绍
Spring Boot(版本号:1.5.10)
Apache Ignite(版本号:2.4.0)数据库
jjwt(版本号:0.9.0)
JDK(版本号:1.8)
Gradle 或 Maven
代码说明:
└── com └── tingfeng ├── AppRun.java (运行入口,包含JWT过滤器配置) ├── config │ ├── IgniteCfg.java (Ignite数据库配置与初始化) │ └── JwtConfig.java (JWT常规配置) ├── controller │ ├── IndexController.java │ ├── PersonController.java (Person注册,登录接口) │ └── SecureController.java (需要token的受限接口) ├── dao │ └── PersonRepository.java (Person增删改查Dao) ├── filter │ └── JwtFilter.java (JWT过滤器,处理与验证JWT的正确性) ├── model │ ├── Person.java │ ├── ReqPerson.java │ └── Role.java └── service ├── PersonService.java └── impl └── PersonServiceImpl.java
AppRun.java,没什么可说的,程序入口
import com.tingfeng.filter.JwtFilter;
import org.apache.ignite.springdata.repository.config.EnableIgniteRepositories;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
* 该项目是用于将Ignite部署到SpringBoot上的一个测试性的项目
* 目前的功能包含:
* 1. 启动并使用一个ignite节点
* 2. 提供api接口实现RESTful的设计,能够通过api添加与查询Cache中的相关内容
*
*/
@SpringBootApplication
@EnableIgniteRepositories
public class AppRun {
/**
* JWT 过滤器配置
*/
@Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/secure/*");
return registrationBean;
}
public static void main(String[] args) {
SpringApplication.run(AppRun.class, args);
}
}我将JWT的过滤器设置在了AppRun这里,如果你不喜欢这种模式,也可以在config包下,创建一个JwtCfg.java的文件,文件内容如下
@Configuration
public class JwtCfg {
@Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/secure/*");
return registrationBean;
}
}JwtFilter 类 这个类声明了一个JWT过滤器类,从Http请求中提取JWT的信息,并使用了"secretkey"这个密匙对JWT进行验证
import com.tingfeng.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
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 javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT 过滤器
*/
public class JwtFilter extends GenericFilterBean {
@Override
public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
//从Http请求获取授权
final String authHeader = request.getHeader("authorization");
// 如果Http请求是OPTIONS,那么只需返回状态码200,即代码中的HttpServletResponse.SC_OK
// 除OPTIONS外,其他请求应由JWT检查
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
chain.doFilter(req, res);
} else {
// Check the authorization, check if the token is started by "Bearer "
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid Authorization header");
}
// 然后从授权中获取JWT令牌
final String token = authHeader.substring(7);
try {
// 使用JWT解析器检查签名是否对密钥“secretkey”有效
final Claims claims = Jwts.parser().setSigningKey(JwtConfig.SECRET_KEY).parseClaimsJws(token).getBody();
System.out.println("claims: " + claims);
// Add the claim to request header
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token");
}
chain.doFilter(req, res);
}
}
}PersonController 类 这个类中在用户进行注册,登录操作成功之后,将生成一个JWT作为返回,如果你不想注册,那么在IgniteCfg中已经初始化了3条数据便于测试,启动时可见
import com.tingfeng.config.JwtConfig;
import com.tingfeng.model.Person;
import com.tingfeng.model.ReqPerson;
import com.tingfeng.model.Role;
import com.tingfeng.service.PersonService;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@RestController
public class PersonController {
@Autowired
private PersonService personService;
/**
* 用户注册
*/
@PostMapping(value = "/register")
public String register(@RequestBody() ReqPerson reqPerson) throws ServletException {
// 检查输入
if (reqPerson.getUsername() == "" || reqPerson.getUsername() == null || reqPerson.getPassword() == "" || reqPerson.getPassword() == null) {
throw new ServletException("Username or Password invalid!");
}
// 检查用户是否已被注册
if (personService.findPersonByUsername(reqPerson.getUsername()) != null) {
throw new ServletException("Username is used!");
}
// 默认权限 : MEMBER
List<Role> roles = new ArrayList<>();
roles.add(Role.MEMBER);
// 创建新的 Person 到 ignite DB
personService.save(new Person(reqPerson.getUsername(), reqPerson.getPassword(), roles));
return "Register Success!";
}
/**
* 检查用户的登录信息,然后创建并返回给前端 jwt token 令牌
*
* @param reqPerson
* @return jwt token
* @throws ServletException
*/
@PostMapping("/login")
public String login(@RequestBody ReqPerson reqPerson) throws ServletException {
// 检查输入
if (reqPerson.getUsername() == "" || reqPerson.getUsername() == null || reqPerson.getPassword() == "" || reqPerson.getPassword() == null) {
throw new ServletException("Please fill in username and password");
}
Person person = personService.findPersonByUsername(reqPerson.getUsername());
// 检查用户是否存在。密码是否正确
if (personService.findPersonByUsername(reqPerson.getUsername()) == null || !reqPerson.getPassword().equals(person.getPassword())) {
throw new ServletException("Please fill in username and password");
}
// 创建 Twt token 令牌,将username,roles写入令牌
String jwtToken = Jwts.builder()
.setSubject(reqPerson.getUsername())
.claim("roles", person.getRoles())
.setIssuedAt(new Date())
.setExpiration(JwtConfig.EXPIRATION_DATE)
.signWith(JwtConfig.SIGNATURE_ALGORITHM, JwtConfig.SECRET_KEY)
.compact();
return jwtToken;
}
}SecureController 类 这个类中只是用于测试JWT功能,当用户认证成功之后,/secure 下的资源才可以被访问
import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 测试JWT,如果验证成功直接返回数据,否则会被过滤器拦截
*/
@RestController
@RequestMapping("/secure")
public class SecureController {
@RequestMapping("/users/user")
public String loginSuccess() {
return "Login Successful!";
}
@PostMapping("/user/roles")
public Object checkRoles(HttpServletRequest request) {
// 从token中获取用户角色
Claims claims = request.getAttribute("claims") != null ? (Claims) request.getAttribute("claims") : null;
return claims.get("roles");
}
}四、代码功能测试
本例使用Postman对代码进行测试,这里并没有考虑到安全性传递的明文密码,实际上应该用SSL进行加密
首先进行一个新的测试用户的注册,可以看到注册成功的提示返回
再让该用户进行登录,可以看到登录成功之后返回的JWT字符串
直接申请访问/secure/users/user ,这时候肯定是无法访问的,服务器返回500错误
将获取到的JWT作为Authorization属性提交(自动添加header),申请访问/secure/users/user ,可以访问成功
上图与下图类似,如果不用属性,手动添加也是一样的,都是请求添加header而已
五、错误记录
SpringBoot在2.0.1版本中会出现IgniteRepository的错误。
Error:(16, 8) java: 名称冲突: org.springframework.data.repository.CrudRepository中的deleteAll(java.lang.Iterable<? extends T>)和org.apache.ignite.springdata.repository.IgniteRepository中的deleteAll(java.lang.Iterable<ID>)具有相同疑符, 但两者均不覆盖对方
也不要使用1.5.11版本,不然会出现下面类型转换问题
com.tingfeng.config.IgniteCfg$$EnhancerBySpringCGLIB$$5b3f3d81 cannot be cast to org.apache.ignite.configuration.IgniteConfiguration
参考地址
未经允许请勿转载:程序喵 » Spring Boot实战之Filter实现使用JWT进行接口认证
程序喵