普通的PC端web程序会话管理是由session
进行管理,但对于微信小程序,APP程序,session
对其支持是有限的,再加上之前由于整个项目往往都是后端一把抓,后端人员要写前端js,还要写服务端程序,工作量十分庞大。若是由专门的前端人员写页面,要观察页面往往也要重启服务程序,前后端关联太紧密。
前后端分离
由于上述的种种弊端,于是有了前后端分离的架构。前端(pc,小程序,app)都可以共用一个服务端程序。pc端的html
可以通过ajax
对服务端进行请求,app中也有着封装的api
可以通过http
协议进行访问,服务端构建起REST风格的api,就可以实现共用后端的需求。
由于之前的会话都是由session进行管理,现在改由token维持会话。下面是客户端与后端进行交互的步骤流程。
- 客户端发送账号密码到服务端
- 服务端校验通过后生成一个token(唯一的字符串)
- 服务端发送token给客户端
- 客户端保存token,具体如何存储可以客户端自己决定
- 客户端发起请求时,将token放入http请求头
- 服务端校验token,就可以知道当前是哪个用户再进行操作
- 服务端返回业务处理的响应结果
示例代码
Token实体类,用于关联token和用户及维持token的有效时间。
@Component
public class Token {
//用户名
private String username;
//token值
private String token;
//到期时间
private Long expire;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
}
下面是Token的工具类,封装了token的产生函数
package com.ay.font_back.utils;
import com.ay.font_back.modal.Token;
import java.security.MessageDigest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/* *
* Created by Ay on 2018/11/20
*/
public class TokenUtil {
//加密的秘钥
private static final char[] hexCode = "0123456789abcdef".toCharArray();
//token有效时间 12小时后过期
private final static int EXPIRE = 3600 * 12;
//模拟数据库存储token
//嫌数据库查询太慢 可以用redis储存
public static Map<String,Token> map = new HashMap<>();
/**
* 调用接口
* @param para
* @return
* @throws Exception
*/
public static Token generateToken(String para) throws Exception {
//产生token
String token = generateToken(para,System.currentTimeMillis());
//当前时间
Date now = new Date();
//到期时间
Date expireTime = new Date(now.getTime()+EXPIRE);
Token token1 = map.get(para);
//未生成过token
if(token1 == null){
token1 = new Token();
token1.setUsername(para);
token1.setToken(token);
token1.setExpire(expireTime.getTime());
map.put(para,token1);
}else {
//更新token
token1.setToken(token);
token1.setExpire(expireTime.getTime());
}
return token1;
}
/**
* 用户的每次访问后 更新过期时间
* @param token
*/
public static void updateExpireTime(Token token){
token.setExpire(System.currentTimeMillis() + EXPIRE);
}
/**
* 生成token
* @param para
* @param timestamp
* @return
* @throws Exception
*/
public static String generateToken(String para,long timestamp) throws Exception {
String text = String.format("%s,%d",para,timestamp);
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(text.getBytes("utf-8"));
return toHexString(bytes);
} catch (Exception e) {
throw new Exception("生成token失败");
}
}
/**
* 字节数组转为16进制
* @param data
* @return
*/
public static String toHexString(byte[] data) {
if(data == null) {
return null;
}
StringBuilder r = new StringBuilder(data.length*2);
for ( byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
}
下面是controller,一个登录接口,一个请求数据接口,在请求时应统一对token进行验证,再完成业务逻辑后更新下token的过期时间,避免频繁地因为token过期重新认证。
@RestController
public class UserController {
@RequestMapping("/user/login")
public String login(@RequestParam("userName") String username,
@RequestParam("password") String password) throws Exception {
//模拟查询数据库 校验用户名和密码
if("Ay".equals(username) && "123456".equals(password)){
Token token = TokenUtil.generateToken(username);
return token.getToken();
}
return "账户密码错误";
}
@RequestMapping("/user/listData")
public String listData(@RequestHeader("ay_token") String token){
//判断token,可以通过 AOP,拦截器,过滤器统一进行权限校验
Token token1 = null;
for(Map.Entry<String,Token> entry :TokenUtil.map.entrySet()){
//有token值存在
if(entry.getValue().getToken().equals(token)){
token1 = entry.getValue();
break;
}
}
if(token1 == null){
return "无权访问";
}
//token 失效
if(System.currentTimeMillis() > token1.getExpire()){
return "请重新进行认证";
}
System.out.println("执行业务逻辑前token的到期时间:"+token1.getExpire());
//查询用户信息,处理业务逻辑 。。。
//aop 完成该操作 业务逻辑完成 更新token的过期时间
TokenUtil.updateExpireTime(token1);
System.out.println("执行业务逻辑后token的到期时间:"+token1.getExpire());
return "这是要请求的数据....";
}
}
前端部分代码,简单地模拟登录和访问具体接口
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
<script src="jquery.js"></script>
</head>
<body>
<button onclick="onLogin()">登录</button><br>
token:<p id="userToken"></p>
<button onclick="getData()">获取数据</button><br>
token:<p id="listData"></p>
<script>
function onLogin(){
jQuery.ajax({
method: "POST",
url: "http://localhost:8080/user/login",
data:
{
"userName":"Ay",
"password":"123456"
},
success: function(data, textStatus, jqXHR)
{
$("#userToken").html(data);
},
error: function( jqXHR, textStatus, errorThrown)
{
trace( "error: " + errorThrown );
}
});
}
function getData(){
jQuery.ajax({
method: "POST",
url: "http://localhost:8080/user/listData",
headers:{
"ay_token":$("#userToken").html()
},
success: function(data, textStatus, jqXHR)
{
$("#listData").html(data);
},
error: function( jqXHR, textStatus, errorThrown)
{
trace( "error: " + errorThrown );
}
});
}
</script>
</body>
</html>
通过测试,程序运行成功,但仅仅是这样的话并不是真正的前后端分离。
静态服务器nginx
为实现真正的前后端分离,需将html/js/css这些静态文件放入nginx的html目录。nginx默认端口是80,通过启动nginx访问前端页面,前端页面再通过ajax访问tomcat上的java服务端程序,这时我们会发现ajax被拒绝访问。
原因是浏览器有一个同源策略,即“协议+域名+端口”三者相同才能发起ajax请求,哪怕是IP地址相同也不行,上面这个情况是端口不同,前端在80,后端在8080,这样ajax访问就属于跨域访问了。
ajax跨域请求访问
ajax在跨域访问的时候会先发送一个options请求,询问服务端是否前端可以对其访问。上面的例子就是因为服务端不允许这个前端进行访问。
CORS解决方案
服务端设置Access-Control-Allow-Origin,如果是要带cookie请求,前后端都要设置,但这里使用的是token,所以只要设置后端即可。
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
spring boot的解决方案,在启动函数内加入这个bean,规定哪些网站可以进行访问。
/**
* 支持跨域请求,允许localhost这个网站来请求user下的接口
* @return
*/
@Bean
public WebMvcConfigurer coreWebMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/**").allowedOrigins("http://localhost");
}
};
}