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

作者: 小柒

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

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

相关文章
|
9天前
|
安全 Java Ruby
我尝试了所有后端框架 — — 这就是为什么只有 Spring Boot 幸存下来
作者回顾后端开发历程,指出多数框架在生产环境中难堪重负。相比之下,Spring Boot凭借内置安全、稳定扩展、完善生态和企业级支持,成为构建高可用系统的首选,真正经受住了时间与规模的考验。
79 2
|
5月前
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
460 1
|
6月前
|
前端开发 Java 关系型数据库
基于Java+Springboot+Vue开发的鲜花商城管理系统源码+运行
基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。技术学习共同进步
429 7
|
6月前
|
人工智能 Java 数据库
飞算 JavaAI:革新电商订单系统 Spring Boot 微服务开发
在电商订单系统开发中,传统方式耗时约30天,需应对复杂代码、调试与测试。飞算JavaAI作为一款AI代码生成工具,专注于简化Spring Boot微服务开发。它能根据业务需求自动生成RESTful API、数据库交互及事务管理代码,将开发时间缩短至1小时,效率提升80%。通过减少样板代码编写,提供规范且准确的代码,飞算JavaAI显著降低了开发成本,为软件开发带来革新动力。
|
6月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
399 70
|
5月前
|
存储 消息中间件 前端开发
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
校园圈子系统校园论坛小程序采用uni-app前端框架,支持多端运行,结合PHP后端(如ThinkPHP/Laravel),实现用户认证、社交关系管理、动态发布与实时聊天功能。前端通过组件化开发和uni.request与后端交互,后端提供RESTful API处理业务逻辑并存储数据于MySQL。同时引入Redis缓存热点数据,RabbitMQ处理异步任务,优化系统性能。核心功能包括JWT身份验证、好友系统、WebSocket实时聊天及活动管理,确保高效稳定的用户体验。
301 4
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
|
5月前
|
供应链 JavaScript BI
ERP系统源码,基于SpringBoot+Vue+ElementUI+UniAPP开发
这是一款专为小微企业打造的 SaaS ERP 管理系统,基于 SpringBoot+Vue+ElementUI+UniAPP 技术栈开发,帮助企业轻松上云。系统覆盖进销存、采购、销售、生产、财务、品质、OA 办公及 CRM 等核心功能,业务流程清晰且操作简便。支持二次开发与商用,提供自定义界面、审批流配置及灵活报表设计,助力企业高效管理与数字化转型。
474 2
ERP系统源码,基于SpringBoot+Vue+ElementUI+UniAPP开发
|
4月前
|
Java API 微服务
Java 21 与 Spring Boot 3.2 微服务开发从入门到精通实操指南
《Java 21与Spring Boot 3.2微服务开发实践》摘要: 本文基于Java 21和Spring Boot 3.2最新特性,通过完整代码示例展示了微服务开发全流程。主要内容包括:1) 使用Spring Initializr初始化项目,集成Web、JPA、H2等组件;2) 配置虚拟线程支持高并发;3) 采用记录类优化DTO设计;4) 实现JPA Repository与Stream API数据访问;5) 服务层整合虚拟线程异步处理和结构化并发;6) 构建RESTful API并使用Springdoc生成文档。文中特别演示了虚拟线程配置(@Async)和StructuredTaskSco
422 0
|
6月前
|
前端开发 JavaScript 关系型数据库
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
275 5
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
|
6月前
|
监控 前端开发 小程序
陪练,代练,护航,代打小程序源码/前端UNIAPP-VUE2.0开发 后端Thinkphp6管理/具备家政服务的综合型平台
这款APP通过技术创新,将代练、家政、娱乐社交等场景融合,打造“全能型生活服务生态圈”。以代练为切入点,提供模块化代码支持快速搭建平台,结合智能匹配与技能审核机制,拓展家政服务和商业管理功能。技术架构具备高安全性和扩展性,支持多业务复用,如押金冻结、录屏监控等功能跨领域应用。商业模式多元,包括交易抽成、增值服务及广告联名,同时设计跨领域积分体系提升用户粘性,实现生态共生与B端赋能。
499 12