Spa—前后端分离+JWT token验权机制
前言
什么是前后端分离?
目前,大家一致认同的前后端分离的例子就是SPA(Single-pageapplication),所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现。 前端:负责View和Controller层。 后端:只负责Model层,业务处理/数据等。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
后端
后端是您无法直接在浏览器中看到的内容。它是处理信息并在后台准备的东西。
必要类:json统一返回值
JsonResult:
/**
* json统一返回值
*/
public class JsonResult {
boolean success;
String message;
Object data=new Object();
public JsonResult() {
}
public JsonResult(boolean success, String message, Object data) {
this.success = success;
this.message = message;
this.data = data;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "\nJsonResult{" +
"success=" + success +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}
JWT token验权机制
引入jwt token依赖
<!-- JWT token -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
将jwt token封装成静态方法
JwtUtil
public static String maketoken(Object obj){
Date now=new Date(); //当前时间
long guoqi=now.getTime()+1000*120; //过期时间2分钟
Date expire=new Date(guoqi); //转型
JwtBuilder builder= Jwts.builder().setId("001")
.setIssuedAt(now) //创建时间
.setExpiration(expire) //过期时间
.claim("login",obj) //可以存对象[Object]
.signWith(SignatureAlgorithm.HS256,"Mr_xiao"); //设置签名 用HS256算法,设置密钥为Mr_xiao
String token=builder.compact();
return token;
}
此处可模拟登陆,也可真实查询数据库验证
这里我用mysql数据库真实查询数据验证
全局配置application.properties
#数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root
#mybatis
#mapper映射路径
mybatis.mapper-locations=classpath:mybatis/mapper/**/*.xml
mybatis.mapper-location=classpath:mybatis/mybatis-config.xml
#mapper映射扫描包路径开启别名
mybatis.type-aliases-package=com.mr_xiao.miaosha.entity
#关闭驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=false
Entity
User用户表根据数据库设计
public class User implements Serializable {
int uid;
String uname;
String upassword;
String uphone;
Date birthday;
public User(int uid, String uname, String upassword, String uphone, Date birthday) {
this.uid = uid;
this.uname = uname;
this.upassword = upassword;
this.uphone = uphone;
this.birthday = birthday;
}
public User(int uid, String uname, Date birthday) {
this.uid = uid;
this.uname = uname;
this.birthday = birthday;
}
public User(String uname, String upassword, String uphone) {
this.uname = uname;
this.upassword = upassword;
this.uphone = uphone;
}
public User() {
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
public String getUpassword() {
return upassword;
}
public void setUpassword(String upassword) {
this.upassword = upassword;
}
public String getUphone() {
return uphone;
}
public void setUphone(String uphone) {
this.uphone = uphone;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "\nUser{" +
"uid=" + uid +
", uname='" + uname + '\'' +
", upassword='" + upassword + '\'' +
", uphone='" + uphone + '\'' +
", birthday=" + birthday +
'}';
}
}
Seckill商品表
/**
* 秒杀商品
*/
public class Seckill {
int seckill_id;
String name;
int number;
String start_time;
String end_time;
String create_time;
public Seckill(int seckill_id, String name, int number, String start_time, String end_time, String create_time) {
this.seckill_id = seckill_id;
this.name = name;
this.number = number;
this.start_time = start_time;
this.end_time = end_time;
this.create_time = create_time;
}
public Seckill() {
}
public int getSeckill_id() {
return seckill_id;
}
public void setSeckill_id(int seckill_id) {
this.seckill_id = seckill_id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getStart_time() {
return start_time;
}
public void setStart_time(String start_time) {
this.start_time = start_time;
}
public String getEnd_time() {
return end_time;
}
public void setEnd_time(String end_time) {
this.end_time = end_time;
}
public String getCreate_time() {
return create_time;
}
public void setCreate_time(String create_time) {
this.create_time = create_time;
}
@Override
public String toString() {
return "\nSeckill{" +
"seckill_id=" + seckill_id +
", name='" + name + '\'' +
", number=" + number +
", start_time='" + start_time + '\'' +
", end_time='" + end_time + '\'' +
", create_time='" + create_time + '\'' +
'}';
}
}
Dao
@Mapper
@Repository
public interface UserDao {
//login
public User getUser(String uname,String upassword);
//查商品
public List getSeckill();
}
Service
public interface UserService {
//login
public User getUser(String uname, String upassword);
//查商品
public List getSeckill();
}
ServiceImpl
@Service
public class UserServiceImpl {
@Autowired
UserDao userDao;
//login
public User getUser(String uname, String upassword){
User u1= userDao.getUser(uname, upassword);
return u1;
}
//Seckill
public List getSeckill(){
List u1= userDao.getSeckill();
return u1;
}
}
Mapper
<mapper namespace="com.mr_xiao.miaosha.dao.UserDao">
<select id="getUser" resultType="User">
select *from user where uname=#{uname} and upassword=#{upassword}
</select>
<select id="getSeckill" resultType="Seckill">
select *from seckill
</select>
</mapper>
基本项目搭建好了
Controller
@RestController
public class MiaoshaController {
@Autowired
UserDao userDao;
//登陆
@RequestMapping("/login")
public JsonResult login(String stname,String stpass){
System.out.println("前台数据:"+stname+"pwd="+stpass);
User u1=userDao.getUser(stname, stpass);
System.out.println("user="+u1);
String token=JwtUtil.maketoken(u1); //封装令牌
if(u1!=null){
return new JsonResult(true,"登陆成功",token);
}else {
return new JsonResult(false,"登陆失败",null);
}
}
//主页商品列表
@RequestMapping("/goods")
public JsonResult goods(String stname,String stpass){
System.out.println("前台数据:"+stname+"pwd="+stpass);
List seckList=userDao.getSeckill();
System.out.println("所有商品="+seckList);
JsonResult json=new JsonResult(true,"查询成功",seckList);
return json;
}
}
拦截器Interceptor
/**
* 登陆拦截
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equals("OPTIONS")) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
System.out.println("进入登陆拦截器...");
String token=request.getHeader("token");
System.out.println("***检查令牌"+token);
Claims claims= Jwts.parser().setSigningKey("Mr_xiao").parseClaimsJws(token).getBody();
System.out.println(claims);
// 出错了会报异常
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
ajax请求json数据难免会遇到跨域问题,所以要解决这个问题—>
WebConfig
/**
* 支持跨域
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
LoginInterceptor login_lj;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(login_lj)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
后台就写好了,前台只需要直接拿数据匹对
前端
前端是与用户界面和交互相关的所有内容。其中包括HTML作为表示信息的语言,CSS作为表示信息的语言的语言以及JavaScript作为交互的语言。
首页index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.app{
background-color: pink;
height: 300px;
}
</style>
<script src="js/jquery.min.js"></script>
</head>
<body>
<h1>SPA 单页面应用程序</h1>
<a href="#">登录页</a>
<a href="#">主页</a>
<hr />
<div class="app">
动态替换
</div>
</body>
<script>
$(function(){
$("a").eq(0).click(function(){
$(".app").load("login.html");
})
$("a").eq(1).click(function(){
$(".app").load("main.html");
})
})
</script>
</html>
登录页Login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>登录页</h1>
<div>
UserName:<input type="text" id="stname" name="stname" /><br/>
PassWord:<input type="password" id="pwd" name="stpass" /><br/>
<button>Login</button>
</div>
</body>
<script>
$(function(){
$("button").click(function(){
var name=$("#stname").val();
var pwd=$("#pwd").val();
//向后台发ajax请求
var url="http://localhost:8080/login"; //绝对路径
var data={
"stname":name,
"stpass":pwd
};
$.post(url,data,function(json){
console.log(json)
//json 服务器返回值
//true 去主页 false提示
if(json.success==true){
alert(json.message)
//存token
localStorage.setItem("mstoken",json.data);
$(".app").load("main.html")
$("a").eq(0).text("退出登录")
}else{
alert(json.message)
}
})
});
});
</script>
</html>
数据页main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>我是主页</h1>
<table border="1">
<tr id="tr1">
<th>商品列表</th>
</tr>
</table>
</body>
<script>
$(function(){
//onload ajax 查商品列表信息
//取出token
var mstoken=localStorage.getItem("mstoken");
$.ajax({
headers:{token: mstoken},
url:"http://localhost:8080/goods",
method:"post",
data:{
"stname":"zhouchen",
"stpass":"66666"
},
success:function(data){
alert(data.message)
if(data.success==true){
for(var i=0;i<data.data.length;i++){
$("#tr1").after("<tr><td>"+data.data[i].name+"</td></tr>")
}
}else{
alert("登陆已失效,请重新登陆")
$(".app").load("login.html")
}
}
})
})
</script>
</html>
首页效果图
在未登录的情况下点击主页并不会显示数据,会被拦截器拦截
效果如下:
前往登陆
ajax请求进行登陆验证,登陆成功
现在即可查询到数据
jwt token密钥失效后便又需重新登陆,否则查看不到数据
这样整个项目就odk了…
前后端分离的好处
- 1、为优质产品打造精益团队. 术业有专攻,通过前后端分离,让前后端工程师只需要专注于前端或者后端的开发工作,培养前端工程师独特的技术特性,然后构建出一个全栈式的精益开发团队。
- 2、提高工作效率,分工更加明确. 前后端分离的工作流程可以使得前端专心前端,后端关心后端,两者开发同时进行,提高工作效率,页面的增加和路由的修改也不必再去麻烦后端,开发更加灵活。
- 3、局部性能提升. 通过前端路由的配置,我们可以实现页面的按需加载,无需一开始加载首页便加载网站的所有资源,服务器也不再需要解析前端页面,在页面交互及用户体验上有所提升。
- 4、增强代码的可维护性.