SpringBoot+JWT+SpringSecurity对api进行授权保护
概述
在生成环境中,我们通常需要对接口加入一些权限认证。我们可以使用session来记录当前请求用户的状态信息加以验证,但是当服务架构从单一演化到集群模式的时候,又会出现session共享问题。
今天我们使用jwt对来解决接口权限认证的问题,在我看来jwt有如下好处。
1. 无状态,不需要占用服务器的资源。
2. 紧凑安全,特别适用于SSO场景,不管单机或者集群,都适用。
3. 高效,无需多次验证,在一次会话中,只需要验证一次。
jwt简介
什么是jwt
JWT(Json Web Token),是一种工具,格式为XXXX.XXXX.XXXX的字符串,JWT以一种安全的方式在用户和服务器之间传递存放在JWT中的不敏感信息。
jwt的构成
jwt是由三部分构成的`Header.Payload.Signature`
header:存放我们的jwt使用生成singnature的算法类型
Payload:Claim里面存放了jwt自身的一些属性,这些属性是用来标注用户的身份属性,比如:JWT的签发着,JWT的创建时间,失效时间,或者用户自定义的属性等等(以下是一个完整的claime)。claime通过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-definde":"用户自定义属性举例"
}
Signature:将上面的Header和Payload以`Header.Payload`的格式组合在一起形成一个字符串,然后使用header里面的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,这个字符串就是Signature。
我们可以登录 https://jwt.io 来decoded我们的jwt
jwt认证过程
代码实现
引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
jwt生成过程
创建一个`JWTLoginFilter`过滤器,创建之后该方法就会默认的过滤客户端的"/login"请求,它主要用于在验证用户名和密码正确之后,生成一个token返回给客户端。该类继承自`UsernamePasswordAuthenticationFilter`主要实现了它的两个方法:
`attemptAuthentication`:解析用户传来的用户信息。
`successfulAuthentication`:用户登陆请求成功校验之后,这个类会被调用,用于生成token
package com.wintig.jwt.filter;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wintig.jwt.entity.MyUser;
import com.wintig.jwt.entity.Response;
import com.wintig.jwt.utils.DateUtils;
import com.wintig.jwt.utils.ResponseUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
//获取前台过来的user信息
MyUser myUser = new ObjectMapper()
.readValue(req.getInputStream(), MyUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
myUser.getUsername(),
myUser.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 请求校验成功后的处理
*/
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) {
HashMap<String, String> result = new HashMap<>();
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setIssuedAt(new Date())
.setExpiration(DateUtils.LocalDateTimeToDate(LocalDateTime.now().plusDays(7))) //设置有效期7天
.signWith(SignatureAlgorithm.HS512, "jwtSecurity")
.compact();
result.put("Authorization", "Bearer " + token);
//将生成token返回给前台
ResponseUtil.write(res, JSON.toJSON(Response.success(result)));
}
}
创建一个实现了`UserDetailsService`方法的类`UserDetailsServiceImpl`,这个类用于处理我们根据前台传来的`username`,来获取我们数据库中的真实用户的信息并返回给`JWTManager`,让jwt来验证我们的用户信息是否正确。
package com.wintig.jwt.service;
import com.wintig.jwt.dao.MyUserRepository;
import com.wintig.jwt.entity.MyUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import static java.util.Collections.emptyList;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private MyUserRepository myUserRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询我们的真实用户,返回给前台
MyUser user = myUserRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), emptyList());
}
}
jwt授权验证过程
当用户登录成功后,会拿到token,在之后所有的请求,带上这个token,服务端就会验证token的合法性。
创建一个`JWTAuthenticationFilter`类,这个类主要用于进行token的校验,`doFilterInternal`方法中,获取http请求头中的`Authorization`读取token数据
package com.wintig.jwt.filter;
import com.alibaba.fastjson.JSON;
import com.wintig.jwt.entity.Response;
import com.wintig.jwt.enums.ResultStatusEnum;
import com.wintig.jwt.utils.ResponseUtil;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authManager) {
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader("Authorization");
// 如果没有带token
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
//如果校验token的时候出现了比如格式问题,或者过期了等问题,返回校验错误结果
ResponseUtil.write(res, JSON.toJSON(Response.failed(ResultStatusEnum.ACCESS_DENIED, "Token Check Error")));
return;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
// 解析token.
String user = Jwts.parser()
.setSigningKey("jwtSecurity")
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody()
.getSubject();
// token验证成功
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
SpringSecurity配置
下面就是SpringSecurity的配置,没什么好说的了,在`antMatchers`方法中加入我们需要进行验证的接口,permitAll就说明当前接口可以具有所有的权限,不需要验证。如果想做更细的验证的话,看看方法名就差不多了。不过现在的互联网项目,大多数做一个登录验证就满足了大部分的需求
package com.wintig.jwt.config;
import com.wintig.jwt.filter.JWTAuthenticationFilter;
import com.wintig.jwt.filter.JWTLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(
"/user/register"
).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTAuthenticationFilter(authenticationManager()));
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
演示
首先我们随便访问一个没有开放权限的接口`/user/hello`,然后报了403错误
下面我们注册一个账户,从上面的配置可以看出用户注册接口`/user/register`,不需要进行权限验证,可以直接访问。我们注册一个账户,接口顺利的就注册成功了。
接下来我们调用`/login`方法,使用我们刚注册的用户进行登陆,可以看到接口给我们返回了token
下面我们根据拿到的token去访问,我们之前没有权限的接口`/user/hello`,这时候会发现接口成功请求了。
总结
以上一个简单的SpringBoot+JWT为API增加授权保护就顺利的完成的,还是很简单的。基本上可以满足日常绝大部分的需求,如果需要更复杂的权限校验,就需要查阅其他教程。
源码地址:https://github.com/wintig/spring-boot-examples/tree/master/spring-boot-jwt