您现在的位置是:首页 > 文章详情

SpringBoot 2.x 开发案例之前后端分离鉴权

日期:2020-04-15点击:318

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

作者: 小柒

出处: https://blog.52itstyle.vip

转载地址https://www.cnblogs.com/smallSevens/p/12712744.html

原文链接:https://yq.aliyun.com/articles/755642
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章