之前基于session的登录方式,是在用户登录成功后将用户信息存入到session中,这样不利于程序的横向扩展, 如果将项目部署多份,会出现session漂移的问题,并且随着登录用户的增加,会不断的占用服务端的内存资源;而现在这种基于token的登录方式,是在登录成功 后 将用户信息存入到客户端中,不会额外占用服务端的内存资源,并且通过签名和验签可以保证数据不被篡改,
又因为登录成功后是将用户的信息存入到客户端中,所以在进行横向扩展,部署多份的时候,不会产生session漂移的问题。
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
写一个简单的登陆测试一下怎么使用token
public class User {
private Long userId;
private String userName;
private String password;
}
package com.fh.jwt;
import com.fh.util.response.ResponseServer;
import com.fh.util.response.ServerEnum;
import io.jsonwebtoken.*;
import sun.misc.BASE64Encoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTokenUtils {
public static String createToken(Map<String,Object> map){
//jwt如何生成字符串
//声明头部信息
Map<String,Object> headerMap=new HashMap<String,Object>();
headerMap.put("alg","HS256");
headerMap.put("typ","JWT");
//设置负载:不要放着涉密的东西比如:银行账号密码,余额,身份证号
Map<String,Object> payload=new HashMap<String,Object>();
payload.putAll(map);
Long iat=System.currentTimeMillis();
//设置jwt的失效时间 一分钟
Long endTime = iat+60000l;
//签名值就是我们的安全密钥
String token=Jwts.builder()
.setHeader(headerMap)
.setClaims(payload)
.setExpiration(new Date(endTime))
.signWith(SignatureAlgorithm.HS256,getSecretKey("secretKey"))
.compact();
return token;
}
public static ResponseServer resolverToken(String token ){
Claims claims=null;
try {
claims = Jwts.parser()
.setSigningKey(getSecretKey("secretKey"))
.parseClaimsJws(token)
.getBody();
}catch (ExpiredJwtException exp){
System.out.println("token超时,token失效了");
return ResponseServer.error(ServerEnum.TOKEN_TIMEOUT);
}catch (SignatureException sing){
System.out.println("token解析失败");
return ResponseServer.error(ServerEnum.SAFETY_ERROR);
}
return ResponseServer.success(claims);
}
private static String getSecretKey(String key){
return new BASE64Encoder().encode(key.getBytes());
}
}
package com.fh.controller;
import com.fh.jwt.JwtTokenUtils;
import com.fh.model.User;
import com.fh.service.UserService;
import com.fh.tokenauth.TokenCheckAnnotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@CrossOrigin
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
public UserService service;
@RequestMapping("login")
public Map<String,Object> login(String userName,String password){
HashMap<String, Object> map = new HashMap<>();
User u = service.queryUser(userName);
if (u == null) {
map.put("code",3000);
return map;
}
// 生成token
Map<String,Object> m = new HashMap<>();
m.put("id",u.getUserId());
String token = JwtTokenUtils.createToken(m);
map.put("token",token);
return map;
}
//@TokenCheckAnnotation 是自定义的注解,在需要进行登录认证的所有方法加上注解
//客户端访问本方法时会拦截
@TokenCheckAnnotation
@RequestMapping("test")
public void test(){
System.out.println("我被调用了");
}
}
package com.fh.tokenauth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 修饰范围
@Retention(RetentionPolicy.RUNTIME) // 用来描述注解的声明周期
public @interface TokenCheckAnnotation {
}
附一个随便找的状态码
就用到了几个,其他的都用不到
package com.fh.util.response;
public enum ServerEnum {
SUCCESS(200,"操作成功"),
DEL_DEPT_SCUCCESS(201,"删除部门成功"),
LOGIN_ISNULL(5000,"用户名或者密码为空"),
PHONE_ISNULL(5007,"手机号不能为空"),
USERNAME_NOTEXIST(5001,"用户名输入有误。"),
PASSWORD_WRONG(5002,"密码输入错误,请检查"),
LOGIN_SUCCESS(5003,"登陆成功"),
LOGIN_EXPIRED(5004,"登录超时,请重新登陆"),
SECRET_ERROR(5005,"传入的token值有误,不能通过签名验证"),
TOKEN_TIMEOUT(5006,"登录失效,请重新登录"),
TOKEN_ISNULL(5008,"获取到的Token值为空"),
NO_MENU_RIGHT(6000,"没有权限访问该菜单,请联系管理员"),
NOT_DATA(7001,"没有要导出的数据"),
HTTP_URL_ISNULL(8002,"你传递的URL路径为空了"),
SERVER_TIMEOUT(8004,"服务连接请求超时"),
HTTP_ERROR(8003,"接口访问失败"),
SERVER_STOP(8005,"服务连接不上"),
SAFETY_ERROR(9000,"接口验签失败"),
SAFETY_BAD(9001,"接口被非法攻击"),
SAFETY_TIMEOUT(9002,"接口访问超时"),
SAFETY_INVALID(9003,"签名值无效"),
SAFETY_REPLAY_ATTACK(9004,"接口被重放攻击"),
LOGIN_PHONEORCODE_INNULL(10000,"手机号或者验证码为空了"),
LOGIN_CODE_ERROR(10001,"手机验证码输入有误"),
ALL_STOCK_NULL(20001,"商品的库存都不足了"),
NO_ORDER_TO_PAY(20002,"没有要支付的订单"),
CRATER_PAY_ERROR(20003,"生成支付二维码失败"),
PAY_TIMEOUT(20004,"支付超时请刷新页面"),
ERROR(500,"操作失败");
private ServerEnum(int code ,String msg){
this.code=code;
this.msg=msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
package com.fh.util.response;
public class ResponseServer {
private Integer code;
private String msg;
private Object data;
private ResponseServer(){
}
private ResponseServer(Integer code,String msg){
this.code=code;
this.msg=msg;
}
private ResponseServer(Integer code,String msg,Object data){
this.code=code;
this.msg=msg;
this.data=data;
}
/**
* 返回默认的 成功状态 200
* @return
*/
public static ResponseServer success(){
return new ResponseServer(ServerEnum.SUCCESS.getCode(),ServerEnum.SUCCESS.getMsg());
}
/**
* 返回默认的带数据 成功状态 200
* @param data
* @return
*/
public static ResponseServer success(Object data){
return new ResponseServer(ServerEnum.SUCCESS.getCode(),ServerEnum.SUCCESS.getMsg(),data);
}
/**
* 其他特殊类型的成功状态,
* @param serverEnum
* @return
*/
public static ResponseServer success(ServerEnum serverEnum){
return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg());
}
/**
* 带返回数据的其他特殊类型的成功状态
* @param serverEnum
* @param data
* @return
*/
public static ResponseServer success(ServerEnum serverEnum,Object data){
return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg(),data);
}
//失败
public static ResponseServer error(Integer code,String msg){
return new ResponseServer(code,msg);
}
/**
+ * @return
*/
public static ResponseServer error(){
return new ResponseServer(ServerEnum.ERROR.getCode(),ServerEnum.ERROR.getMsg());
}
/**
* 返回默认的带数据 失败状态 500
* @param data
* @return
*/
public static ResponseServer error(Object data){
return new ResponseServer(ServerEnum.ERROR.getCode(),ServerEnum.ERROR.getMsg(),data);
}
/**
* 其他特殊类型的失败状态,
* @param serverEnum
* @return
*/
public static ResponseServer error(ServerEnum serverEnum){
return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg());
}
/**
* 带返回数据的其他特殊类型的失败状态
* @param serverEnum
* @param data
* @return
*/
public static ResponseServer error(ServerEnum serverEnum,Object data){
return new ResponseServer(serverEnum.getCode(),serverEnum.getMsg(),data);
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
public Object getData() {
return data;
}
public void setCode(Integer code) {
this.code = code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public void setData(Object data) {
this.data = data;
}
}
验证异常
package com.fh.exception;
import com.fh.util.response.ServerEnum;
public class AuthenticateException extends RuntimeException{
private Integer code;
public AuthenticateException(ServerEnum serverEnum) {
super(serverEnum.getMsg());
this.code=serverEnum.getCode();
}
public Integer getCode() {
return code;
}
}
全局异常处理
package com.fh.controller;
import com.fh.exception.AuthenticateException;
import com.fh.util.response.ResponseServer;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthenticateException.class)
public ResponseServer authenticateException(AuthenticateException e, HttpServletRequest request, HttpServletResponse response){
return ResponseServer.error(e.getCode(),e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseServer exceptionHandler(Exception e,HttpServletRequest request, HttpServletResponse response){
e.printStackTrace();
return ResponseServer.error();
}
}
package com.fh.tokenauth;
import com.fh.exception.AuthenticateException;
import com.fh.jwt.JwtTokenUtils;
import com.fh.util.response.ResponseServer;
import com.fh.util.response.ServerEnum;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect // 作用是把当前类标识为一个切面供容器读取
@Component // 把当前类交给spring管理
public class TokenAuthenticateAop {
//切点表达式 aop横切的是 com.fh.controller 包下的 任意类 任意方法 任意参数
@Around(value = "execution(* com.fh.controller.*.*(..)) && @annotation(tokenCheckAnnotation)")
public Object tokenAuth(ProceedingJoinPoint joinPoint, TokenCheckAnnotation tokenCheckAnnotation){
Object proceed = null;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String token = request.getHeader("Authorization-token");
// 验证token是否为空
if (!StringUtils.isNotBlank(token)){
throw new AuthenticateException(ServerEnum.TOKEN_ISNULL);
}
// 验证token是否失效
ResponseServer responseServer = JwtTokenUtils.resolverToken(token);
if (responseServer.getCode() != 200){
throw new AuthenticateException(ServerEnum.LOGIN_EXPIRED);
}
// 执行目标方法
try {
proceed = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
前端采用vue+Element Ui
npm install vuex --save
在新建一个store文件夹
新建index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//声明一个token常量
const store={
state:{
'token':""
}
}
export default new Vuex.Store({
store,
mutations:{
//给常量赋值
set_token(state,token){
state.token=token;
localStorage.setItem("token",token);
}
}
,getters:{
token : state => state.token
}
})
import store from './store'
new Vue({
el: '#app',
router,
store, // 使vuex生效
components: {
App },
template: ' '
})
新建一个Login文件夹 在里面建一个Login.vue
前端对密码进行了MD5加密,所有需要提前在main.js把MD5导入进来
import md5 from 'js-md5'; // 导入MD5
Vue.prototype.$md5 = md5;
<template>
<div >
<el-form ref="form" :model="form" :rules="rules" label-width="80px" class="login-box demo-ruleForm" >
<h3 class="login-title">欢迎登录</h3>
<el-form-item label="用户名" prop="userName"> <!-- prop:将该字段设置为需检验 -->
<el-input v-model="form.userName"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="form.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit('form')">登录</el-button>
<el-button @click="resetForm('form')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "Login",
data(){
return{
form:{
userName:'',
password:'',
},
rules: {
userName: [
{
required: true, message: '请输入用户名', trigger: 'blur' },
{
min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
],
password: [
{
required: true, message: '请输入密码', trigger: 'blur' }
],
}
}
},
mounted(){
//绑定事件
window.addEventListener('keydown',this.keyDown);
},
methods:{
keyDown(e){
//如果是回车则执行登录方法
if(e.keyCode == 13){
this.onSubmit('form');
}
},
onSubmit(formName){
this.$refs[formName].validate((valid) => {
var aa = this
if (valid) {
this.form.password = this.$md5(this.form.password)
this.$axios.post("http://localhost:80/user/login",this.$qs.stringify(this.form)).then(res=>{
if(res.data.code != 3000){
// console.log(res.data.token)
// 登录成功以后将token放到vuex中
aa.$store.commit("set_token",res.data.token);
aa.$router.push("/house");
}else{
aa.$alert("用户名或密码不正确!")
}
}).catch(err => {
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(form){
this.$refs[form].resetFields();
}
},
}
</script>
<style scoped>
.login-box{
width: 600px;
border: 1px solid #DCDFE6;
margin: 180px auto;
padding: 35px 35px 15px 35px;
border-radius: 30px;
box-shadow: 0 0 10px aqua;
}
.login-title{
text-align: center;
margin: 0 auto 30px auto;
color: #00FFFF;
}
</style>
新建house文件夹 创建一个登录成功之后跳转的页面index.vue
<template>
<div>
<h1>我是House</h1>
<el-button type="primary" @click="onSubmit()">点我测试</el-button>
</div>
</template>
<script>
export default {
name: "index",
methods:{
onSubmit(){
this.axios({
method:"get",
url: "http://localhost:80/user/test",
})
}
}
}
</script>
<style scoped>
</style>
在router中配置路由
import Vue from 'vue'
import Router from 'vue-router'
import House from '@/views/house/index'
import Login from '@/views/login/Login'
import store from '../store'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Login',
component: () => import('@/views/login/Login'),
},
{
path: '/house',
name: 'House',
component: () => import('@/views/house/index'),
}
]
})
import Vue from 'vue'
import Router from 'vue-router'
import House from '@/views/house/index'
import Login from '@/views/login/Login'
import store from '../store'
Vue.use(Router)
if(localStorage.getItem("token")){
store.commit("set_token",localStorage.getItem("token"));
}
const router = new Router({
routes: [
{
path: '/',
name: 'Login',
component: () => import('@/views/login/Login'),
},
{
path: '/house',
name: 'House',
component: () => import('@/views/house/index'),
meta:{
requireAuth:true
}
}
]
})
//store.state.token
router.beforeEach((to,form,next)=>{
if(to.matched.some(r=>r.meta.requireAuth)){
//console.log(store.state.token);
//if(store.state.token){
if(localStorage.getItem("token") != null){
next();
}else{
next({
//跳转到登陆页面
path:"/"
})
}
}else{
next();
}
})
export default router;
@LoginAnnotation
@TokenCheckAnnotation
@RequestMapping("test")
public void test(){
System.out.println("我被调用了");
}
对登录的请求进行拦截,判断是否登录
通过查看token里面是否有从后端传过来的token值,如果有就说明已登录,可以继续访问
如果值为空,就是没登陆,重定向的登录页面
同时也要判断token是否已经失效,如果失效也需要重定向到登录页面
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import axios from 'axios'//导入axios
import VueAxios from 'vue-axios'
import ElementUI from 'element-ui'//导入elementui
import md5 from 'js-md5'; // 导入MD5
import 'element-ui/lib/theme-chalk/index.css'
import moment from 'moment'
import qs from 'qs'
import store from './store'
Vue.use(VueAxios,axios)
Vue.use(ElementUI)
// 发送axios请求之前将token设置到请求的头信息里
axios.defaults.headers.common["Authorization-token"] = store.getters.token
// 在请求拦截器中判断头信息中有没有token,防止token没有传到后台
axios.interceptors.request.use(config=>{
alert(store.state.token)
if(store.state.token){
config.headers.common['Authorization-token']=store.state.token;
}
return config;
})
// 判断后台返回的状态码,token失效后跳转到登录页面
axios.interceptors.response.use(response => {
var code = response.data.code;
if(code == 5004 || code == 5006){
router.replace({
path: '/',
query: {
redirect: router.currentRoute.fullPath}//登录成功后跳入浏览的当前页面
})
}
return response;
})
Vue.prototype.$axios=axios
Vue.prototype.$qs=qs
Vue.prototype.$md5 = md5;
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: {
App },
template: ' '
})
End