前言

阅读本文需要一定的前后端开发基础,前后端分离已成为互联网项目开发的业界标准使用方式,通过Nginx代理+Tomcat的方式有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,小程序,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。

其核心思想是前端页面通过AJAX调用后端的API接口并使用JSON数据进行交互。

原始模式

开发者通常使用Servlet、Jsp、Velocity、Freemaker、Thymeleaf以及各种框架模板标签的方式实现前端效果展示。通病就是,后端开发者从后端撸到前端,前端只负责切切页面,修修图,更有甚者,一些团队都没有所谓的前端。

分离模式

在传统架构模式中,前后端代码存放于同一个代码库中,甚至是同一工程目录下。页面中还夹杂着后端代码。前后端分离以后,前后端分成了两个不同的代码库,通常使用 Vue、React、Angular、Layui等一系列前端框架实现。

权限校验

回到文章的主题,这里我们使用目前最流行的跨域认证解决方案JSON Web Token(缩写 JWT

pom.xml引入:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

工具类,签发JWT,可以存储简单的用户基础信息,比如用户ID、用户名等等,只要能识别用户信息即可,重要的角色权限不建议存储:

/**
* JWT加密和解密的工具类
*/
public class JwtUtils {
/**
* 加密字符串 禁泄漏
*/
public static final String SECRET = "e3f4e0ffc5e04432a63730a65f0792b0";
public static final int JWT_ERROR_CODE_NULL = 4000; // Token不存在
public static final int JWT_ERROR_CODE_EXPIRE = 4001; // Token过期
public static final int JWT_ERROR_CODE_FAIL = 4002; // 验证不通过 /**
* 签发JWT
* @param id
* @param subject
* @param ttlMillis
* @return String
*/
public static String createJWT(String id, String subject, long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject) // 主题
.setIssuer("爪哇笔记") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
/**
* 验证JWT
* @param jwtStr
* @return CheckResult
*/
public static CheckResult validateJWT(String jwtStr) {
CheckResult checkResult = new CheckResult();
Claims claims;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(JWT_ERROR_CODE_EXPIRE);
checkResult.setSuccess(false);
} catch (SignatureException e) {
checkResult.setErrCode(JWT_ERROR_CODE_FAIL);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(JWT_ERROR_CODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
} /**
* 密钥
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decode(SECRET);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
} /**
* 解析JWT字符串
* @param jwt
* @return
* @throws Exception Claims
*/
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}

验证实体信息:

/**
* 验证信息
*/
public class CheckResult {
private int errCode; private boolean success; private Claims claims; public int getErrCode() {
return errCode;
} public void setErrCode(int errCode) {
this.errCode = errCode;
} public boolean isSuccess() {
return success;
} public void setSuccess(boolean success) {
this.success = success;
} public Claims getClaims() {
return claims;
} public void setClaims(Claims claims) {
this.claims = claims;
}
}

拦截访问配置,跨域访问设置以及请求拦截过滤:

/**
* 拦截访问配置
*/
@Configuration
public class SafeConfig implements WebMvcConfigurer { @Bean
public SysInterceptor myInterceptor(){
return new SysInterceptor();
} @Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE","OPTIONS")
.allowCredentials(false).maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] patterns = new String[] { "/user/login","/*.html"};
registry.addInterceptor(myInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(patterns);
}
}

拦截器统一权限校验:

/**
* 认证拦截器
*/
public class SysInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(SysInterceptor.class); @Autowired
private SysUserService sysUserService; @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler){
if (handler instanceof HandlerMethod){
String authHeader = request.getHeader("token");
if (StringUtils.isEmpty(authHeader)) {
logger.info("验证失败");
print(response,Result.error(JwtUtils.JWT_ERROR_CODE_NULL,"签名验证不存在,请重新登录"));
return false;
}else{
CheckResult checkResult = JwtUtils.validateJWT(authHeader);
if (checkResult.isSuccess()) {
/**
* 权限验证
*/
String userId = checkResult.getClaims().getId();
HandlerMethod handlerMethod = (HandlerMethod) handler;
Annotation roleAnnotation= handlerMethod.getMethod().getAnnotation(RequiresRoles.class);
if(roleAnnotation!=null){
String[] role = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).value();
Logical logical = handlerMethod.getMethod().getAnnotation(RequiresRoles.class).logical();
List<String> list = sysUserService.getRoleSignByUserId(Integer.parseInt(userId));
int count = 0;
for(int i=0;i<role.length;i++){
if(list.contains(role[i])){
count++;
if(logical==Logical.OR){
continue;
}
}
}
if(logical==Logical.OR){
if(count==0){
print(response,Result.error("无权限操作"));
return false;
}
}else{
if(count!=role.length){
print(response,Result.error("无权限操作"));
return false;
}
}
}
return true;
} else {
switch (checkResult.getErrCode()) {
case SystemConstant.JWT_ERROR_CODE_FAIL:
logger.info("签名验证不通过");
print(response,Result.error(checkResult.getErrCode(),"签名验证不通过,请重新登录"));
break;
case SystemConstant.JWT_ERROR_CODE_EXPIRE:
logger.info("签名过期");
print(response,Result.error(checkResult.getErrCode(),"签名过期,请重新登录"));
break;
default:
break;
}
return false;
}
}
}else{
return true;
}
}
/**
* 打印输出
* @param response
* @param message void
*/
public void print(HttpServletResponse response,Object message){
try {
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setHeader("Cache-Control", "no-cache, must-revalidate");
response.setHeader("Access-Control-Allow-Origin", "*");
PrintWriter writer = response.getWriter();
writer.write(JSONObject.toJSONString(message));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

配置角色注解,可以直接把安全框架Shiro的拷贝过来,如果有需要,菜单权限也可以配置上:

/**
* 权限注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles { /**
* A single String role name or multiple comma-delimitted role names required in order for the method
* invocation to be allowed.
*/
String[] value(); /**
* The logical operation for the permission check in case multiple roles are specified. AND is the default
* @since 1.1.0
*/
Logical logical() default Logical.OR;
}

模拟演示代码:

@RestController
@RequestMapping("/user")
public class UserController {
/**
* 列表
* @return
*/
@RequestMapping("/list")
@RequiresRoles(value="admin")
public Result list() {
return Result.ok("十万亿个用户");
} /**
* 登录
* @return
*/
@RequestMapping("/login")
public Result login() {
/**
* 模拟登录过程并返回token
*/
String token = JwtUtils.createJWT("101","爪哇笔记",1000*60*60);
return Result.ok(token);
}
}

前端请求模拟,发送请求之前在Header中附带token信息,更多代码见源码案例:

function login(){
$.ajax({
url : "/user/login",
type : "post",
dataType : "json",
success : function(data) {
if(data.code==0){
$.cookie('token', data.msg);
}
},
error : function(XMLHttpRequest, textStatus, errorThrown) { }
});
}
function user(){
$.ajax({
url : "/user/list",
type : "post",
dataType : "json",
success : function(data) {
alert(data.msg)
},
beforeSend: function(request) {
request.setRequestHeader("token", $.cookie('token'));
},
error : function(XMLHttpRequest, textStatus, errorThrown) { }
});
}
</script>

安全说明

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期建议设置的相对短一些。对于一些比较重要的权限,使用时应该再次对用户进行数据库认证。为了减少盗用,JWT 强烈建议使用 HTTPS 协议传输。

由于服务器不保存用户状态,因此无法在使用过程中注销某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

源码案例

https://gitee.com/52itstyle/safe-jwt

SpringBoot 2.x 开发案例之前后端分离鉴权的更多相关文章

  1. SpringBoot电商项目实战 — 前后端分离后的优雅部署及Nginx部署实现

    在如今的SpringBoot微服务项目中,前后端分离已成为业界标准使用方式,通过使用nginx等代理方式有效的进行解耦,并且前后端分离会为以后的大型分布式架构.弹性计算架构.微服务架构.多端化服务(多 ...

  2. springBoot 搭建web项目(前后端分离,附项目源代码地址)

    springBoot 搭建web项目(前后端分离,附项目源代码地址)   概述 该项目包含springBoot-example-ui 和 springBoot-example,分别为前端与后端,前后端 ...

  3. Angular企业级开发(9)-前后端分离之后添加验证码

    1.背景介绍 团队开发的项目,前端基于Bootstrap+AngularJS,后端Spring MVC以RESTful接口给前端调用.开发和部署都是前后端分离.项目简单部署图如下,因为后台同时采用微服 ...

  4. Laravel 中使用 swoole 项目实战开发案例二 (后端主动分场景给界面推送消息)

    推荐阅读:Laravel 中使用 swoole 项目实战开发案例一 (建立 swoole 和前端通信)​ 需求分析 我们假设有一个需求,我在后端点击按钮 1,首页弹出 “后端触发了按钮 1”.后端点了 ...

  5. SpringBoot+Vue豆宝社区前后端分离项目手把手实战系列教程01---搭建前端工程

    豆宝社区项目实战教程简介 本项目实战教程配有免费视频教程,配套代码完全开源.手把手从零开始搭建一个目前应用最广泛的Springboot+Vue前后端分离多用户社区项目.本项目难度适中,为便于大家学习, ...

  6. 无需CORS,用nginx解决跨域问题,轻松实现低代码开发的前后端分离

    近年来,前后端分离已经成为中大型软件项目开发的最佳实践. 在技术层面,前后端分离指在同一个Web系统中,前端服务器和后端服务器采用不同的技术栈,利用标准的WebAPI完成协同工作.这种前后端分离的&q ...

  7. CoreCRM 开发实录 —— 前后端分离的重构

    虽然2月初就回来了,可 CoreCRM 一直到5月才开始恢复开发,期间是各种生活中的意外和不方便. 1. 为什么要重构 首先是一件很值得高兴的事情:CoreCRM 有了第一位 contributor! ...

  8. SpringBoot+Vue豆宝社区前后端分离项目手把手实战系列教程02---创建后端工程

    本节代码开源地址 代码地址 项目运行截图 搭建后端工程 0.导入sql 在数据库导入 /* Navicat Premium Data Transfer Source Server : localhos ...

  9. SpringBoot 2.0 开发案例之百倍级减肥瘦身之旅

    前言 为了存我的小黄图,最近在做一款图床服务,集成了各种第三方云存储服务,目前正在内部测试阶段.项目是以Jar的形式运行在腾讯云上,不要问我为什么使用腾讯云了,因为阿里云老用户和狗不得入内. 问题凸显 ...

随机推荐

  1. 【Java面试题】List如何一边遍历,一边删除?

    这是最近面试时被问到的1道面试题,本篇博客对此问题进行总结分享. 1. 新手常犯的错误 可能很多新手(包括当年的我,哈哈)第一时间想到的写法是下面这样的: public static void mai ...

  2. rpc框架dubbo学习入门及环境搭建(spring boot+Kotlin)

    学习之前,确保有以下知识基础: Java网络编程 Socket传输数据 IO流 rpc简介及实现 rpc是remote procedure call的简写,意思为远程过程调用. rpc应用较多的情景是 ...

  3. 曹工说Spring Boot源码(23)-- ASM又立功了,Spring原来是这么递归获取注解的元注解的

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. 【Java】反射调用与面向对象结合使用产生的惊艳

    缘起 我在看Spring的源码时,发现了一个隐藏的问题,就是父类方法(Method)在子类实例上的反射(Reflect)调用. 初次看到,感觉有些奇特,因为父类方法可能是抽象的或私有的,但我没有去怀疑 ...

  5. go语言周边

    博主收藏的go语言资料,分享一波~~~ 官网 https://golang.org/ (被墙) 镜像: http://docscn.studygolang.com/ 下载镜像: https://gom ...

  6. python浅学【网络服务中间件】之RabbitMQ

    一.关于AMQP: AMQP,即Advanced Message Queuing Protocol,高级消息队列协议. AMQP使符合要求的客户端应用程序能够与符合要求的消息传递中间件代理进行通信. ...

  7. hdu1078 dfs+dp(记忆化搜索)搜索一条递增路径,路径和最大,起点是(0,0)

    #include<bits/stdc++.h> using namespace std; typedef unsigned int ui; typedef long long ll; ty ...

  8. 【笔记3-26】Python语言基础

    编译型语言和解释型语言 编译型语言 C 先编译 解释型语言 Python 边执行边编译 Python的介绍 吉多·范罗苏姆 1991 解释型语言 Life is short you need Pyth ...

  9. Secret Milking Machine POJ - 2455 网络流(Dinic算法---广搜判断+深搜增广)+时间优化+二分

    题意: 第一行输入N M C ,表示从1到N有M条无向边,现在要从1走到N 走C次完全不同的路径,求最长边的最小值.下面M行是从a点到b点的距离. 建图: 题上说从两点之间可以有多条边,问的是从1~N ...

  10. 线程安全,syncronized 用法

    1,为什么有线程安全问题? 当多个线程同时共享同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题.但是做读操作是不会发生数据冲突问题. public class Tra ...