使用token进行用户身份验证
参考文章:
写的很详细,具体讲解了token的原理.
解析token的过程:https://www.jianshu.com/p/6bfeb86885a3
为什么使用token
以前都是使用session:
当用户第一次通过浏览器使用用户名和密码访问服务器时,服务器会验证用户数据,验证成功后在服务器端写入session数据,向客户端浏览器返回sessionid,浏览器将sessionid保存在cookie中,当用户再次访问服务器时,会携带sessionid,服务器会拿着sessionid从数据库获取session数据,然后进行用户信息查询,查询到,就会将查询到的用户信息返回,从而实现状态保持。
这种方式对服务器压力大;并且不能很好的支持分布式,(多台服务器的session丢失问题);最后一点:不能防止CSRF跨站伪造请求攻击,由于session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
token的优点:
token与session的不同主要:
①认证成功后,会对当前用户数据进行加密,生成一个加密字符串token,返还给客户端(服务器端并不进行保存);
②浏览器会将接收到的token值每次访问都要携带,并在服务端验证token的真实性;
③再次进行认证,实现状态保持,不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。
使用jwt实现token的验证
Json web token(JWT)是为了网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC 7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
流程:
流程是这样的:
1.用户使用用户名密码请求服务器
2.服务器进行验证用户信息
3.服务器通过验证发送给用户一个token
4.客户端存储token,并在每次请求时附加这个token值
5.服务器验证token,并返回数据
简单的dome
引入依赖
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
全局异常
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(Exception.class)
public String ex(Exception e) {
return e.getMessage();
}
}
创建tokenUtils
public class TokenUtil {
/**
* 加密的秘钥,相当于服务器私钥,一定保管好,不能泄露
*/
private static final String secret = "secret";
/**
* 获取token的key,一般token存在请求头和相应头中
*/
public static final String tokenHeader = "tokenHeader";
/**
* token的有效时间,不需要自己验证失效,当失效后,会自动抛出异常
*/
private static final Long expTime = 60 * 2 * 1000L;
/**
* 创建一个token
* @param id
* @return
*/
public static String getToken(Integer id, String name) {
//也可以添加这些之外的其他信息------------------------|
//这里额外添加一个信息,尝试获取
String uuid = "这里是额外的信息,UUIDkey";
Map<String, Object> map = new HashMap<>();
map.put("uuidkey", uuid);
//也可以添加这些之外的其他信息------------------------|
JwtBuilder builder = Jwts.builder();
String token = builder
//设置加密的方式
.signWith(SignatureAlgorithm.HS256, secret)
//设置生成token的关键信息,可以将关键的信息加密
.setId(uuid).setSubject(name)
//设置token的签发时间和实效的时间
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expTime))
//这个方法实际上是调用的构造方法,so在获取改内容时,就直接将Claims按照map处理
.addClaims(map)
//生成秘钥
.compact();
System.out.println("token=" + token);
return token;
}
/**
* 获取用户的姓名
* @param token
*/
public static String getUserName(String token){
String name = getTokenBody(token).getSubject();
System.out.println("name====" + name);
return name;
}
/**
* 获取用户的id
* @param token
*/
public static String getUserId(String token){
String id = getTokenBody(token).getId();
System.out.println("id====" + id);
return id;
}
/**
* 获取用户的uuidkey
* @param token
*/
public static String getUUIDKey(String token,String key){
String UUIDkey = (String) getTokenBody(token).get(key);
System.out.println("UUIDkey====" + UUIDkey);
return UUIDkey;
}
/**
* 查看并解析token
* 这个方法会在token异常的时候自动抛出异常,不用自定异常,
* 只需要在验证的时候进行捕获即可
* @param token
* @return
*/
public static Claims getTokenBody(String token){
//这里得到是token中的载荷部分,也是具体信息的所在
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token).getBody();
//对应的是上边的
Object uuidkey = claims.get("uuidkey");
System.out.println(uuidkey);
return claims;
}
//测试是否可以去到信息
public static void main(String[] args) {
String token = getToken(1, "mmm");
System.out.println(token);
String uuidkey = getUUIDKey(token, "uuidkey");
//配置进去的是 uuidkey='这里是额外的信息,UUIDkey'
System.out.println(uuidkey);
}
}
另外说明一下创建token的方法的相关的参数:
在第二部分载荷中jwts提供了 JWT标准 7个保留声明(Reserved claims)的设置方法,7个声明都是可选的,也就是说可以不用设置。
setIssuer():iss: 签发者
setSubject():sub: 面向用户
setAudience():aud: 接收者
setExpiration():exp(expires): 过期时间
setNotBefore():nbf(not before):不能被接收处理时间,在此之前不能被接收处理
setIssuedAt():iat(issued at): 签发时间
setId():jti:JWT ID为web token提供唯一标识
设置拦截器验证token
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取token
String token = request.getHeader(TokenUtil.tokenHeader);
System.out.println("token===================="+token);
if (null==token) {
response.sendRedirect("http://127.0.0.1:8080/login");
System.out.println("没有携带token");
return false;
}
try {
TokenUtil.getTokenBody(token);
} catch (Exception e) {
e.printStackTrace();
response.sendRedirect("http://127.0.0.1:8080/login");
System.out.println("token有问题");
return false;
}
return true;
}
}
拦截器的配置,以及静态资源的获取
@Configuration
public class TokenConfig implements WebMvcConfigurer {
@Bean
public TokenInterceptor tokenInterceptor() {
return new TokenInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor())
.addPathPatterns("/**")
//不拦截login和静态资源
.excludePathPatterns("/login","/static/**");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
}
准备测试
登录的controller
@Controller
public class TokenController{
@Autowired
private UserService userService;
@GetMapping("/login")
public String login() {
return "login";
}
@PostMapping(value = "/login")
@ResponseBody
public Object login(HttpServletRequest request, HttpServletResponse response,String username) throws LoginException {
User user = userService.getUserByName(username);
if (null==user) {
throw new LoginException("没有此人");
}
//生成一个token返回或者是放进响应头中
String token = TokenUtil.getToken(user.getId(), user.getName());
return token;
}
}
前端的简陋的页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<script src="/static/jquery-3.4.1.min.js"></script>
<script>
/*设置token为全局变量*/
var token = ''
function fun02() {
var name = $("#name").val()
var password = $("#password").val()
$.ajax({
url: "/login",
type: "post",
data: {
username: name,
password: password
},
success: function (data) {
token = data
alert(token)
}
})
}
function fun01() {
$.ajax({
url: "/ajax",
type: "get",
data: {},
headers: {'tokenHeader': token},
success: function (data) {
alert(data)
}
})
}
</script>
</head>
<body>
<form id="login">
姓名:<input type="text" name="username" id="name"><br>
密码:<input type="password" name="password" id="password"><br>
<input type="button" value="登录" onclick="fun02()">
</form>
<br>
<input type="button" value="ajax请求" onclick="fun01()">
</body>
</html>
测试
当没有登录的时候:当前登录页面下不登录就点击ajax请求会302重定向回当前的页面.说明没有token的时候,只能登录.
在当前的页面下进行登录.登录后将服务端返回的token存入全局变量中,方便所有的请求都要携带在请求头.
当有了token之后再次点击ajax请求,就会访问成功
要点:
基于token的验证在工具包中已经验证,只能由jwts抛出异常判断token异常,所以将验证的代码放入try中捕获异常,并且同时抛出,触发全局异常.
各位可以自行验证.
token解释
两个点号把jwt分成了3部分,分别对应着头部,载荷,签证。
Jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法,通常直接使用HMACSHA256,就是HS256了
然后将头部进行base64编码构成了第一部分:
然后我们看第二部分:载荷。这里是承载的意思。也就是说这里是承载消息具体内容的地方。需要注意的是,不要存放敏感信息,不要存放敏感信息,不要存放敏感信息!!!因为:这里也是base64编码,任何人获取到jwt之后都可以解码!!
只剩下最后一部分签证了,其实就是一个签名信息,使用了自定义的一个密钥然后加密后的结果,目的就是为了保证签名的信息没有被别人改过!(也就是保证jwt安全可用),
头部那里我们不是定义了一个加密算法么,就是它
也就是说,签证部分的信息有3个组成部分:
- 头部-header (base64后的)
- 载荷-payload (base64后的)
- 密钥-secret
然后HMACSHA256只有两个参数,
- base64后的头部 + “.” + base64后的载荷
- 密钥-secret
总结:
基于token的验证很简单,主要是做好前后端的协同,同时要对token的关键的信息进行加密,防止泄露,本代码中并没有加入加密.