SpringBoot 2.x 开发案例之前后端分离鉴权
SpringBoot 2.x 开发案例之前后端分离鉴权
前言
阅读本文需要一定的前后端开发基础,前后端分离已成为互联网项目开发的业界标准使用方式,通过Nginx代理+Tomcat的方式有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,小程序,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。
其核心思想是前端页面通过AJAX调用后端的API接口并使用JSON数据进行交互。
原始模式
开发者通常使用Servlet、Jsp、Velocity、Freemaker、Thymeleaf以及各种框架模板标签的方式实现前端效果展示。通病就是,后端开发者从后端撸到前端,前端只负责切切页面,修修图,更有甚者,一些团队都没有所谓的前端。
分离模式
在传统架构模式中,前后端代码存放于同一个代码库中,甚至是同一工程目录下。页面中还夹杂着后端代码。前后端分离以后,前后端分成了两个不同的代码库,通常使用 Vue、React、Angular、Layui等一系列前端框架实现。
权限校验
回到文章的主题,这里我们使用目前最流行的跨域认证解决方案JSON Web Token(缩写 JWT)
pom.xml引入:
<groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version>
工具类,签发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) { } });
}
安全说明
JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期建议设置的相对短一些。对于一些比较重要的权限,使用时应该再次对用户进行数据库认证。为了减少盗用,JWT强烈建议使用 HTTPS 协议传输。
由于服务器不保存用户状态,因此无法在使用过程中注销某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
源码案例
https://gitee.com/52itstyle/safe-jwt
作者: 小柒
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
答好友困惑:Java零基础如何入门,不知道怎么学,迷茫ING
答好友困惑:Java零基础如何入门,不知道怎么学,迷茫ING 几个星期之前,我在知乎上看到一个提问,说是:对于完全没有经验零基础自身的数学底子也很弱学习Java应该怎么学习呢?想着类似的问题我也有过回答,并且反馈还是蛮好的,就参考之前的思路回答了一遍,可惜没在公众号里和大家分享,后续再整理一份好好分享下。(该问答地址见原文) 提出问题的是一位高中生,就顺藤摸瓜加了我好友,让我给指点指点。跃哥最近飘了,各种给人指点,也不知道是对是错,但是我还是从自身的角度来分析问题,毕竟我也工作多年,给初学者一些信心是我该做的;给初学者一些指导,是我力所能及的;给初学者一些劝退,也是我要做的,毕竟很多人可能最后发现自己并不适合写程序。 期间这位老弟和我聊了很多,我都零零散散给了解答。直到前几天,他抛给我很多困惑,都是在初学的时候会面临到的,所以我就抽时间做了一次详细的解答,主要涉及到Java从入门到进阶需要经历哪些、如何学习Java入门、还没开始实践就有一堆烦恼,该怎么办? 本文将以问答的方式,给出一些简单的见解,因为Java内容挺多的,可能会有遗漏,我已经让群里的小伙伴们做过一次筛选,读者朋友们看到了...
- 下一篇
震撼!全网第一张源码分析全景图揭秘Nginx
震撼!全网第一张源码分析全景图揭秘Nginx 不管是C/C++技术栈,还是PHP,Java技术栈,从事后端开发的朋友对nginx一定不会陌生。 想要深入学习nginx,阅读源码一定是非常重要的一环,但nginx源码量毕竟还是不算少,一不小心就容易陷入某个细节,迷失在茫茫码海之中。 如果有一张地图,让我们开启上帝视角,总览全局,帮助我们快速学习整体框架结构,又能不至于迷失其中那就再好不过了! 看到这篇文章的你有福了,笔者花了不少时间,把这件事给做了,先来看个全貌(限于平台图片尺寸设定,这里只能看个大概,想获取高清大图请看文末): 下面选取一些关键部分来一窥神秘的nginx。 主进程启动nginx主进程启动后,进行一系列的初始化,包括但不限于: 命令行参数解析时间初始化日志初始化ssl初始化操作系统相关初始化一致性hash表初始化模块编号处理 核心初始化另外一个最重要的初始化由ngx_init_cycle()函数完成,该函数围绕nginx中非常核心的一个全局数据结构ngx_cycle_t展开。 该函数完成了几个核心初始化: 配置文件解析创建并监听socket初始化nginx各模块 ngin...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,CentOS7官方镜像安装Oracle11G
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- SpringBoot2全家桶,快速入门学习开发网站教程