我先接触的前后端分离是.Net的webapi,特性路由什么的,所以想知道java中的webapi是什么样的,在网上直接查java webapi
得不到类似于C# 的webapi的资料,但是查java 前后端分离,就能找到类似于C# webapi的东西。
看了一篇文章,根据文章中提供的github地址拉取了源码,源码和文章中的代码很不一样,然后我就综合原文和拉取的代码,以
及运行的过程中发现的问题以及最终的解决方法整理了下面的文章。不过我看的那篇文章对前后端分离的部分的几乎没说什么,
主要讲了为什么要前后端分离以及不少身份验证的知识,生成token,验证token,拦截器什么的。
以前服务端为什么能识别用户呢?对,是session,每个session都存在服务端,浏览器每次请求都带着sessionId(就是一个字符串),于是服务器根据这个sessionId就知道是哪个用户了。
那么问题来了,用户很多时,服务器压力很大,如果采用分布式存储session,又可能会出现不同步问题,那么前后端分离就很好的解决了这个问题。
前后端分离思想:
在用户第一次登录成功后,服务端返回一个token回来,这个token是根据userId进行加密的,密钥只有服务器知道,然后浏览器每次请求都把这个token放在Header里请求,这样服务器只需进行简单的解密就知道是哪个用户了。这样服务器就能专心处理业务,用户多了就加机器。当然,如果非要讨论安全性,那又有说不完的话题了。
下面通过SpringBoot框架搭建一个后台,进行token构建
1.项目的概览
目录结构:
为了尽可能简单,就不连数据库了,登陆时用固定的。
原文并没有从头开始讲创建项目的过程,但是既然是创建springboot项目所以基本过程应该是:
File——New——Project——Spring Initializer,
点击next:
然后接着填写信息或更改信息
点击next:
然后选择依赖,这一步就很重要,整合mybatis,使用thymeleaf,开发web特性的项目等都可以在此时选好依赖。
创建前后端分离的springboot的后端项目,要添加哪些依赖呢?后面会提到pom.xml,那个代码是从拉取的源码中粘贴过来的,
依赖选好了,就可以直接点击next:
然后填写好项目名称以及项目位置,就可以点击finish了。
pom.xml里的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jimo</groupId>
<artifactId>auth-jimo</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
<name>AuthServer</name>
<description>auth server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.UserController类代码:
这里的加密密钥是:base64EncodedSecretKey
package com.jimo.controller;
import com.jimo.model.User;
import com.jimo.model.common.Result;
import com.jimo.security.JwtUtil;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletException;
/**
* @author jimo
* @func controller
* @date 2018/8/24 22:44
*/
@RestController
@RequestMapping("/user")//类似于C# Webapi中的特性路由
public class UserController {
/**
* @func 测试时先用死的用户名密码,请求使用JSON格式数据
* @author wangpeng
* @date 2018/8/24 22:45
*/
@PostMapping("/login") //类似于C# Webapi中的特性路由
public Result login(@RequestBody User user) throws ServletException {
if (!"admin".equals(user.getUsername())) {
throw new ServletException("no such user");
}
if (!"1234".equals(user.getPassword())) {
throw new ServletException("wrong password");
}
return new Result(JwtUtil.getToken(user.getUsername()));
}
/**
* @func 用于客户端检查token是否合法
* @author wangpeng
* @date 2018/8/27 16:58
*/
@PostMapping("/checkToken")
public Result checkToken(String token) {
return new Result(JwtUtil.isTokenOk(token));
}
@GetMapping("/success")
public Result success() {
return new Result("login success");
}
@GetMapping("/getEmail")
public Result getEmail() {
return new Result("[email protected]");
}
}
3.GlobalExceptionHandler类代码:
package com.jimo.exp;
import com.jimo.model.common.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author jimo
* @func 全局异常处理
* @date 2018/8/24 22:44
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return new Result(false, e.getMessage());
}
}
4.Result类代码:
package com.jimo.model.common;
/**
* @author jimo
* @func 封装统一的返回数据
* @date 2018/8/24 22:46
*/
public class Result {
/**
* 成功为true
*/
private boolean ok;
/**
* 错误消息或其他提示
*/
private String msg;
/**
* 数据
*/
private Object data;
public Result() {
this(true, "", null);
}
public Result(Object data) {
this(true, "", data);
}
public Result(boolean ok, String msg) {
this(ok, msg, null);
}
public Result(boolean ok, String msg, Object data) {
this.ok = ok;
this.msg = msg;
this.data = data;
}
public boolean isOk() {
return ok;
}
public void setOk(boolean ok) {
this.ok = ok;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
5.User类代码:
package com.jimo.model;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
6.JwtInterceptor类代码:
package com.jimo.security;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author jimo
* @func 拦截token并验证,不通过则抛出异常
* @date 2018/8/24 22:38
*/
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("prehandle");
final String authorization = request.getParameter("Authorization");
/*String authHeader = request.getHeader("Authorization");*/
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new ServletException("invalid Authorization header,请重新登陆");
}
//取得token
String token = authorization.substring(7);
try {
JwtUtil.checkToken(token);
return true;
} catch (Exception e) {
throw new ServletException(e.getMessage());
}
}
}
7.JwtUtil类代码:
package com.jimo.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.servlet.ServletException;
import java.util.Date;
/**
* @author jimo
* @func Jwt相关
* @date 17-12-12 下午5:28
*/
public class JwtUtil {
/**
* 私钥
*/
final static String base64EncodedSecretKey = "base64EncodedSecretKey";
/**
* 过期时间,测试使用20分钟
*/
final static long TOKEN_EXP = 1000 * 60 * 20;
public static String getToken(String userName) {
return Jwts.builder()
.setSubject(userName)
.claim("roles", "user")
.setIssuedAt(new Date())
/*过期时间*/
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_EXP))
.signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey)
.compact();
//return Jwts.builder().setSubject(userName).claim("roles", "user").setIssuedAt(new Date())
// .signWith(SignatureAlgorithm.HS256, "base64EncodedSecretKey").compact();
}
/**
* @func 检查token, 只要不正确就会抛出异常
* @author jimo
* @date 17-12-12 下午6:21
*/
static void checkToken(String token) throws ServletException {
try {
final Claims claims = Jwts.parser().setSigningKey(base64EncodedSecretKey).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e1) {
throw new ServletException("token expired");
} catch (Exception e) {
throw new ServletException("other token exception");
}
}
/**
* @func token ok返回true
* @author wangpeng
* @date 2018/8/27 16:59
*/
public static boolean isTokenOk(String token) {
try {
Jwts.parser().setSigningKey(base64EncodedSecretKey).parseClaimsJws(token).getBody();
return true;
} catch (Exception e) {
return false;
}
}
}
在调用login()方法后,会生成一个token,然后用这个token调用success方法应该会提示"login success",但是事情没有想的那么
顺利:
我用生成的token去调用success方法时,总是提示"invalid Authorization header,请重新登陆"。然后我发现,
上述代码在postman中输入http://localhost:8081/user/login以及admin和1234,每一次请求都会产生不同的Token,
所以我认为当输入http://localhost:8081/user/login,且填入新生成的token时,验证失败。而且我以为要想解决这个问题要先解决
下述问题:
怎么能在生成一个token后可以在一段相对较长的时间内使用这个token调用其他方法?
不过,很快我就发现还有一个问题,我发现或者说注意到,每次返回的信息都是:
{
"ok": false,
"msg": "invalid Authorization header,请重新登陆",
"data": null
}
为什么不是提示token过期呢?
通过查看代码可以知道,“invalid Authorization header,请重新登陆”这条信息只有在token为空或者格式不正确的时候才会提示,
token过期应该提示的是“token expired”。而上述问题应该属于token过期啊。
上述两个问题有没有什么关系?
我添加了两行代码(都是在控制台输出token的),我想要看看能不能取到前端传来的token,如果能取到,是什么 样的?
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("prehandle");
final String authorization = request.getParameter("Authorization");
/*String authHeader = request.getHeader("Authorization");*/
//test
System.out.println(authorization);//输出的是null
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new ServletException("invalid Authorization header,请重新登陆");
}
//取得token
String token = authorization.substring(7);
//test
System.out.println(token);//程序没能运行到这里
try {
JwtUtil.checkToken(token);
return true;
} catch (Exception e) {
throw new ServletException(e.getMessage());
}
}
}
为什么会这样?输出是null没有得到数据!
代码里获取数据的代码是:request.getParameter("Authorization")
还要注释掉的获取数据的代码:request.getHeader("Authorization")
“Authorization”是在Parameter还是在Header,还是两者都不是,而是其他?
postman是这样的,Authorization是放在Headers里面的:
而代码里调用的确实getParameter()方法,如果改成getHeader()会如何?
问题解决了!而且是两个问题都解决了。