安全认证--JWT介绍及使用
Posted 伏加特遇上西柚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安全认证--JWT介绍及使用相关的知识,希望对你有一定的参考价值。
安全认证--JWT介绍及使用
1.无状态登录原理
1.1.什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
1.2.什么是无状态
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备
自描述信息
,通过这些信息识别客户端身份
带来的好处是什么呢?
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩
- 减小服务端存储压力
1.3.如何实现无状态
无状态登录的流程:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效。
整个登录过程中,最关键的点是什么?
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用JWT
来生成token,保证token的安全性
1.4.JWT
1.4.1.简介
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
GitHub上jwt的java客户端:https://github.com/jwtk/jjwt
1.4.2.数据格式
JWT包含三部分数据:
-
Header:头部,通常头部有两部分信息:
- token类型,这里是JWT
- 签名算法,自定义
我们会对头部进行base64加密(可解密),得到第一部分数据
-
Payload:载荷,就是有效数据,一般包含下面信息:
- 标准载荷:JWT规定的信息,jwt的元数据:
- JTI: JWT的id,当前jwt的唯一标识(像身份证号)
- IAT: issue at 签发时间
- EXP:过期时间
- SUB:签发人
- …
- 自定义载荷:
- 用户身份信息,(注意,这里因为采用base64加密,可解密,因此不要存放敏感信息)
这部分也会采用base64加密,得到第二部分数据
- 标准载荷:JWT规定的信息,jwt的元数据:
-
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
生成的数据格式:
可以看到分为3段,每段就是上面的一部分数据。
什么是 JWT – JSON WEB TOKEN
傻傻分不清之 Cookie、Session、Token、JWT
JWT详细教程与使用
2.编写JWT工具
我们会用到比较流行的java语言的JWT工具,jjw
2.1.添加JWT依赖
我们需要先在项目中引入JWT依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--json工具-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--日期时间工具类-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2.载荷对象
JWT中,会保存载荷数据,我们计划存储2部分:
- jti:jwt的id
- UserDetail:用户数据
为了方便后期获取,我们定义一个类来封装。
添加一个实体类,代表载荷信息
import lombok.Data;
//载荷对象
@Data
public class Payload
/**
* tocken的id
*/
private String jti;
/**
* 用户数据
*/
private UserDetail userDetail;
载荷中的UserDetail信息,也需要一个实体类表示,这里我们定义一个UserDetail类。
这里我们假设用户信息包含2部分:
- id:用户id
- username:用户名
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserDetail
/**
* 用户id
*/
private Long id;
/**
* 用户名
*/
private String username;
2.3.工具
我创建一个JwtUtils 工具类,用来封装几个方法:
- createJwt() :生成JWT
- parseJwt() :验证并解析JWT
import com.example.jwt.constants.RedisConstants;
import com.example.jwt.dto.Payload;
import com.example.jwt.dto.UserDetail;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class JwtUtils
/**
* JWT解析器
*/
private final JwtParser jwtParser;
/**
* 秘钥
*/
private final SecretKey secretKey;
// @Autowired
private StringRedisTemplate redisTemplate;
private final static ObjectMapper mapper = new ObjectMapper();
public JwtUtils(String key)
// 生成秘钥
secretKey = Keys.hmacShaKeyFor(key.getBytes(Charset.forName("UTF-8")));
// JWT解析器
this.jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
/**
* 生成jwt,自己指定的JTI
*
* @param userDetails 用户信息
* @return JWT
*/
public String createJwt(UserDetail userDetails)
try
// 生成tokenid
String jti=createJti();
//存入redis中
// this.redisTemplate.opsForValue().set(RedisConstants.JTI_KEY_PREFIX+userDetails.getUsername(),jti,30, TimeUnit.MINUTES);
return Jwts.builder().signWith(secretKey)
.setId(jti)
.claim("user", mapper.writeValueAsString(userDetails))
.compact();
catch (JsonProcessingException e)
throw new RuntimeException(e);
/**
* 解析jwt,并将用户信息转为指定的Clazz类型
*
* @param jwt token
* @return 载荷,包含JTI和用户信息
*/
public Payload parseJwt(String jwt)
try
Jws<Claims> claimsJws = jwtParser.parseClaimsJws(jwt);
Claims claims = claimsJws.getBody();
Payload payload = new Payload();
payload.setJti(claims.getId());
payload.setUserDetail(mapper.readValue(claims.get("user", String.class), UserDetail.class));
return payload;
catch (IOException e)
throw new RuntimeException(e);
private String createJti()
return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
/**
* 刷新jwt有效期
* @param username
*/
public void refreshJwt(String username)
String key= RedisConstants.JTI_KEY_PREFIX+username;
//重置key的过期时间
redisTemplate.expire(key,30,TimeUnit.MINUTES);
2.4.测试
2.4.1.配置秘钥
在application.yml
文件中配置秘钥:
yy:
jwt:
key: helloWorldJavaAuthServiceSecretKe
定义一个配置类,注册JwtUtils
注入到Spring的容器。
import com.example.jwt.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JwtConfig
@Value("$yy.jwt.key")
private String key;
@Bean
public JwtUtils jwtUtils()
return new JwtUtils(key);
2.4.2.测试类
@Autowired
private JwtUtils jwtUtils;
@Test
public void test()
// 生成jwt
String jwt = jwtUtils.createJwt(UserDetail.of(112L, "lele"));
System.out.println("jwt = " + jwt);
// 解析jwt
Payload payload = jwtUtils.parseJwt(jwt);
System.out.println("payload = " + payload);
结果:
jwt = eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyZTRkMGI1NjZiODY0YjUzODAwZTI3NGNhOWE0MTcxYSIsInVzZXIiOiJ7XCJpZFwiOjExMixcInVzZXJuYW1lXCI6XCJsZWxlXCJ9In0.NGa42tISwsLg_hyONasdGPGDigFFxkWbH04wd4ELztY
payload = Payload(jti=2e4d0b566b864b53800e274ca9a4171a, userDetail=UserDetail(id=112, username=lele))
2.5项目源码
3.jwt优秀介绍
JWT详细教程与使用
傻傻分不清之 Cookie、Session、Token、JWT
什么是 JWT – JSON WEB TOKEN
spring boot整合jwt 实现前后端分离登录认证及授权
一丶 基本介绍
前后端分离的认证及授权有两种方式,
第一种是使用jwt 也就是(Json Web Token),客户端请求服务端,完成账号密码的认证以后,由服务端生成一个带有过期时间的token,返回给客户端,后续每次请求客户端都要带上这个token,服务端从请求中拿到token 进行解析 判断是否过期,然后构建spring security的安全对象,交由spring security框架进行后续的认证等处理.这种方式相比于传统的session方式不同,是无状态的,服务端没有保存和每个客户端对应的session对象,而是由客户端每次请求带上token,服务端进行解析 来判断客户端的身份,这相比传统方式对服务端的压力非常小,不需要保存和每个客户端对应的session对象,而且由于前后端分离,后端更加倾向于提供接口,很多业务逻辑前移,后端只需要认证请求的身份,提供好对应的接口,剩下的权限控制,跳转页面等就交由前端实现.
第二种 就是spring cloud的OAuth2认证方式,我这里没有去研究,所以就不细说了.
我这里也不过多介绍jwt了 百度相关的文章很多,我就直接介绍spring boot怎么整合jwt实现登录认证及授权
首先贴出maven依赖
1 <!-- jwt依赖 -->
<dependency> 2 <groupId>io.jsonwebtoken</groupId> 3 <artifactId>jjwt</artifactId> 4 <version>0.9.0</version> 5 </dependency>
<!--spring security的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
暂时就引入这两个依赖吧,一个是jwt,另外一个呢是spring security的依赖
二丶 代码实现
操作jwt生成token有一个现成的工具类,已经写好了常用方法,比如根据用户名生成token,计算token过期时间等方法,我这里先把这个工具类贴上来
1 import io.jsonwebtoken.Claims; 2 import io.jsonwebtoken.Jwts; 3 import io.jsonwebtoken.SignatureAlgorithm; 4 import org.springframework.beans.factory.annotation.Value; 5 import org.springframework.security.core.userdetails.UserDetails; 6 import org.springframework.stereotype.Component; 7 8 import java.io.Serializable; 9 import java.util.Date; 10 import java.util.HashMap; 11 import java.util.Map; 12 import java.util.function.Function; 13 14 /** 15 * @Description: JwtTokenUtil,JWT工具类,生成/验证/是否过期token 。 16 * @Author: Tan 17 * @CreateDate: 2019/12/2 18 **/ 19 @Component 20 public class JwtTokenUtil implements Serializable { 21 private static final long serialVersionUID = -2550185165626007488L; 22 23 //token有效期 24 @Value("${jwt.validity}") 25 private Long tokenValidity; 26 27 28 //加密秘钥 29 @Value("${jwt.secret}") 30 private String secret; 31 32 //通过token返回用户名 33 public String getUsernameFromToken(String token) { 34 return getClaimFromToken(token, Claims::getSubject); 35 } 36 37 //通过token得到token过期时间 38 public Date getExpirationDateFromToken(String token) { 39 return getClaimFromToken(token, Claims::getExpiration); 40 } 41 42 //从token中获得用户信息 43 public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { 44 final Claims claims = getAllClaimsFromToken(token); 45 return claimsResolver.apply(claims); 46 } 47 48 //从token中解密 获得用户信息 49 private Claims getAllClaimsFromToken(String token) { 50 return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); 51 } 52 53 //验证token是否过期 54 private Boolean isTokenExpired(String token) { 55 final Date expiration = getExpirationDateFromToken(token); 56 return expiration.before(new Date()); 57 } 58 59 //根据用户生成token 60 public String generateToken(UserDetails userDetails) { 61 Map<String, Object> claims = new HashMap<>(); 62 return doGenerateToken(claims, userDetails.getUsername()); 63 } 64 65 //生成token 66 private String doGenerateToken(Map<String, Object> claims, String subject) { 67 return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) 68 .setExpiration(new Date(System.currentTimeMillis() + (tokenValidity * 1000))) 69 .signWith(SignatureAlgorithm.HS512, secret).compact(); 70 } 71 72 //验证token 73 public Boolean validateToken(String token, UserDetails userDetails) { 74 final String userName = getUsernameFromToken(token); 75 return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token)); 76 } 77 78 }
tokenValidity这个token有效期和secret加密秘钥,这两个变量是通过读取spring boot的application.yml配置文件中定义的,在spring Ioc容器实例化这个类的实例的时候就会从配置文件中读取,这样不写死,也方便后续修改
到这里关于jwt的代码实现其实已经结束了,已经可以生成token,和验证token了,接下来就是关于spring security的配置部分了,其实spring security相比于另外一个安全框架shiro来说绝对算是重量级,也比较复杂,但是呢由于是spring提供,搭配整个spring生态使用应该还是可以的
首先spring security对用户的操作,比如登录判断用户名密码是否正确,访问某个资源是否有对应的权限,定义了一个接口 或许也有类吧 但是我是实现了接口 重写了那些方法 就算是满足了spring security要求的安全用户对象,在它内部的实现机制就会用到,我们只需要传参构建好这个对象即可
1 import org.springframework.security.core.GrantedAuthority; 2 import org.springframework.security.core.authority.SimpleGrantedAuthority; 3 import org.springframework.security.core.userdetails.UserDetails; 4 5 import java.util.Collection; 6 import java.util.List; 7 import java.util.stream.Collectors; 8 9 /** 10 * @Description: 实现 UserDetails 重写方法 就是满足spring security安全要求的用户 11 * spring security验证用户必须要使用实现UserDetails的类,的实例 12 * 所以构建这个类 将我们自身实体类中的一些字段 赋值到这个类 用于校验 13 * 也是由于我们自身的用户实体类 字段比较多 14 * @Author: Tan 15 * @CreateDate: 2019/12/6 16 **/ 17 public class SecurityUser implements UserDetails { 18 //用户名 19 private String userName; 20 //密码 21 private String passWord; 22 //权限集合 23 private Collection<? extends GrantedAuthority> authoritys; 24 //是否可用 25 private boolean enabled; 26 27 /** 28 * @Description: 这个构造方法 从用户实体对象中给这个安全用户赋值 29 * @Author: Tan 30 * @Date: 2019/12/6 31 * @param userName: 用户账号 32 * @param passWord: 用户密码 33 * @param authority: 用户权限集合 34 * @param enabled: 用户是否可用 35 * @return: null 36 **/ 37 public SecurityUser (String userName, String passWord, List<String> authority,boolean enabled ){ 38 this.userName=userName; 39 this.passWord=passWord; 40 this.enabled=enabled; 41 this.authoritys =authority.stream().map(item->new SimpleGrantedAuthority(item)).collect(Collectors.toList()); 42 } 43 44 @Override 45 public Collection<? extends GrantedAuthority> getAuthorities() { 46 return this.authoritys; 47 } 48 49 @Override 50 public String getPassword() { 51 return this.passWord; 52 } 53 54 @Override 55 public String getUsername() { 56 return this.userName; 57 } 58 59 @Override 60 public boolean isAccountNonExpired() { 61 return true; 62 } 63 64 @Override 65 public boolean isAccountNonLocked() { 66 return true; 67 } 68 69 @Override 70 public boolean isCredentialsNonExpired() { 71 return true; 72 } 73 74 @Override 75 public boolean isEnabled() { 76 return this.enabled; 77 } 78 }
这个类只定义了用户名,密码,拥有的权限和是否可用,其实还可以定义几个属性,比如该用户是否未锁定,是否未过期,密码是否未过期,这里我就没有写了 在重写的方法里面默认返回都是true,这个类写好了,在别的地方会实例化的.
其实为什么不使用和数据库对应的User实体类来实现这个接口,因为和数据库对应的User实体类肯定是有很多无关的字段,所以还是单独建一个类,将用户名,密码这些值传进来进行构建比较好.
接下来编写一个类实现一个接口重写一个方法,后续spring security框架会将得到的用户名调用这个方法,我们可以在这个方法里面将得到的用户名去数据库查询出是否有对应的记录,如果有记录就构建上面这个类的对象,传入用户名,密码,权限集合,是否可用然后返回
如果不存在这个记录,直接抛出一个用户名不存在异常 UsernameNotFoundException,我们这里返回了一个有用户名,密码,权限集合的对象,如果是登录的话,spring security框架会将这个用户名密码和从请求里面得到的token中解析出来的用户进行匹配 如果匹配失败
就会响应401错误,把代码贴出来
1 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 2 import com.tqq.eggchat.dao.UserMapper; 3 import com.tqq.eggchat.entity.SecurityUser; 4 import com.tqq.eggchat.entity.User; 5 import lombok.extern.slf4j.Slf4j; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.security.core.userdetails.UserDetails; 8 import org.springframework.security.core.userdetails.UserDetailsService; 9 import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 import org.springframework.stereotype.Service; 11 12 import java.util.Arrays; 13 14 /** 15 * @Description: 这个类实现UserDetailsService接口 成为满足spring security标准的用户业务类 16 * 提供根据用户名 返回 UserDetails对象的方法 17 * 这里可以注入dao类对象 查询数据库 对应的用户信息 然后构造UserDetails对象 18 * @Author: Tan 19 * @CreateDate: 2019/12/6 20 **/ 21 @Slf4j 22 @Service 23 public class SecurityService implements UserDetailsService { 24 25 @Autowired 26 private UserMapper userMapper; 27 28 /** 29 * @Description: 根据用户名去数据库查询对应的用户信息 30 * @Author: Tan 31 * @Date: 2019/12/6 32 * @param userName: 用户名 33 * @return: org.springframework.security.core.userdetails.UserDetails 34 **/ 35 @Override 36 public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { 37 //根据用户名查询用户信息 38 User user = userMapper.selectOne(new QueryWrapper<User>().eq("s_account", userName)); 39 if(user!=null){ 40 //这里暂时没有权限的概念 就默认个user权限 41 return new SecurityUser(user.getS_account(),user.getS_password(), Arrays.asList("USER"),user.getI_status()==1?true:false); 42 }else{ 43 log.info("查询数据库,该账号{}不存在",userName); 44 throw new UsernameNotFoundException(String.format("%s 该账号不存在",userName)); 45 } 46 } 47 }
我这个@Slf4j是lombok框架提供的一个功能,相当于是一个日志对象,省得重复写了,直接在代码中就可以用,在编译以后会加上的.要想使用这个功能除了要引用lombok框架的依赖,使用的IDE也要装插件才能使用
客户端每次请求都会带上token.那么就需要一个过滤器,从请求对象中获取token 然后进行解析等,把过滤器代码贴出来
1 /** 2 * @Description: 这个过滤器用于判断请求中是否有token 如果有就进行登录到spring security中 3 * 继承OncePerRequestFilter 这个类是spring 对filter的封装 可以实现 4 * 一次请求 只会执行一次过滤器 5 * @Author: Tan 6 * @CreateDate: 2019/12/6 7 **/ 8 @Component 9 public class JwtRequestFilter extends OncePerRequestFilter { 10 11 @Autowired 12 private SecurityService securityService; 13 14 @Autowired 15 private JwtTokenUtil jwtTokenUtil; 16 17 @Override 18 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 19 String tokenHead=request.getHeader("Authorization"); 20 //token头不等于空 并且以Bearer 开头进行token验证登录处理 21 if(tokenHead!=null&&tokenHead.startsWith("Bearer ")){ 22 //从请求头中截取token 23 String token=tokenHead.substring(7); 24 //通过token得到用户名 如果token已过期或者错误 会抛出异常,并被spring security捕获 调我们自定义的登录失败处理方法 25 String userName = jwtTokenUtil.getUsernameFromToken(token); 26 //用户名不等于空 并且当前上下文环境中没有认证过 就进行登录验证 27 if(userName!=null&& SecurityContextHolder.getContext().getAuthentication()==null){ 28 //通过用户名查询数据库 构建符合spring security要求的安全用户对象 29 UserDetails userDetails = securityService.loadUserByUsername(userName); 30 //验证token和用户对象 31 if(jwtTokenUtil.validateToken(token,userDetails)){ 32 //通过安全用户对象 构建一个登录对象 33 UsernamePasswordAuthenticationToken login=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()); 34 //传入当前http请求对象 35 login.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 36 //将登录对象 写入到当前上下文环境中 后续的判断 权限控制就由spring Security做 37 SecurityContextHolder.getContext().setAuthentication(login); 38 } 39 //调用下一个过滤器 如果有token已经在此完成登录 没有登录的话 会被后续拦截器处理 40 filterChain.doFilter(request,response); 41 } 42 43 }
在这个过滤器里面有注入了操作jwt的工具类和之前定义的用于根据用户名查询数据库构建spring security要求的用户对象的类,
以上是关于安全认证--JWT介绍及使用的主要内容,如果未能解决你的问题,请参考以下文章
双十一钜惠:科技人的专属保障移动端认证保障 ——非对称加密及jwt应用
#私藏项目实操分享# Spring专题「开发实战」Spring Security与JWT实现权限管控以及登录认证指南