Spring Boot Security 整合 JWT 实现 无状态的分布式API接口
简介
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。JSON Web Token 入门教程 - 阮一峰,这篇文章可以帮你了解JWT的概念。本文重点讲解Spring Boot 结合 jwt ,来实现前后端分离中,接口的安全调用。
快速上手
之前的文章已经对 Spring Security 进行了讲解,这一节对涉及到 Spring Security 的配置不详细讲解。若不了解 Spring Security 先移步到 Spring Boot Security 详解。
建表
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `role`;
DROP TABLE IF EXISTS `user_role`;
DROP TABLE IF EXISTS `role_permission`;
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `user_role` (
`user_id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL
);
CREATE TABLE `role_permission` (
`role_id` bigint(11) NOT NULL,
`permission_id` bigint(11) NOT NULL
);
CREATE TABLE `permission` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`description` varchar(255) NULL,
`pid` bigint(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO role (id, name) VALUES (1,'USER');
INSERT INTO role (id, name) VALUES (2,'ADMIN');
INSERT INTO permission (id, url, name, pid) VALUES (1,'/user/hi','',0);
INSERT INTO permission (id, url, name, pid) VALUES (2,'/admin/hi','',0);
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
项目结构
resources
|___application.yml
java
|___com
| |____gf
| | |____SpringbootJwtApplication.java
| | |____config
| | | |____.DS_Store
| | | |____SecurityConfig.java
| | | |____MyFilterSecurityInterceptor.java
| | | |____MyInvocationSecurityMetadataSourceService.java
| | | |____MyAccessDecisionManager.java
| | |____entity
| | | |____User.java
| | | |____RolePermisson.java
| | | |____Role.java
| | |____mapper
| | | |____PermissionMapper.java
| | | |____UserMapper.java
| | | |____RoleMapper.java
| | |____utils
| | | |____JwtTokenUtil.java
| | |____controller
| | | |____AuthController.java
| | |____filter
| | | |____JwtTokenFilter.java
| | |____service
| | | |____impl
| | | | |____AuthServiceImpl.java
| | | | |____UserDetailsServiceImpl.java
| | | |____AuthService.java
关键代码
pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>
application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring-security-jwt?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //校验用户
        auth.userDetailsService( userDetailsService ).passwordEncoder( new PasswordEncoder() {
            //对密码进行加密
            @Override
            public String encode(CharSequence charSequence) {
                System.out.println(charSequence.toString());
                return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
            }
            //对密码进行判断匹配
            @Override
            public boolean matches(CharSequence charSequence, String s) {
                String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
                boolean res = s.equals( encode );
                return res;
            }
        } );
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                //因为使用JWT,所以不需要HttpSession
                .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //OPTIONS请求全部放行
                .antMatchers( HttpMethod.OPTIONS, "/**").permitAll()
                //登录接口放行
                .antMatchers("/auth/login").permitAll()
                //其他接口全部接受验证
                .anyRequest().authenticated();
        //使用自定义的 Token过滤器 验证请求的Token是否合法
        http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
        http.headers().cacheControl();
    }
    @Bean
    public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtTokenFilter();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
JwtTokenUtil
/**
 * JWT 工具类
 */
@Component
public class JwtTokenUtil implements Serializable {
    private static final String CLAIM_KEY_USERNAME = "sub";
    /**
     * 5天(毫秒)
     */
    private static final long EXPIRATION_TIME = 432000000;
    /**
     * JWT密码
     */
    private static final String SECRET = "secret";
    /**
     * 签发JWT
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(16);
        claims.put( CLAIM_KEY_USERNAME, userDetails.getUsername() );
        return Jwts.builder()
                .setClaims( claims )
                .setExpiration( new Date( Instant.now().toEpochMilli() + EXPIRATION_TIME  ) )
                .signWith( SignatureAlgorithm.HS512, SECRET )
                .compact();
    }
    /**
     * 验证JWT
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        String username = getUsernameFromToken( token );
        return (username.equals( user.getUsername() ) && !isTokenExpired( token ));
    }
    /**
     * 获取token是否过期
     */
    public Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken( token );
        return expiration.before( new Date() );
    }
    /**
     * 根据token获取username
     */
    public String getUsernameFromToken(String token) {
        String username = getClaimsFromToken( token ).getSubject();
        return username;
    }
    /**
     * 获取token的过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        Date expiration = getClaimsFromToken( token ).getExpiration();
        return expiration;
    }
    /**
     * 解析JWT
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey( SECRET )
                .parseClaimsJws( token )
                .getBody();
        return claims;
    }
}
JwtTokenFilter
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    /**
     * 存放Token的Header Key
     */
    public static final String HEADER_STRING = "Authorization";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader( HEADER_STRING );
        if (null != token) {
            String username = jwtTokenUtil.getUsernameFromToken(token);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                            request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}
AuthServiceImpl
@Service
public class AuthServiceImpl implements AuthService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Override
    public String login(String username, String password) {
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
        Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        UserDetails userDetails = userDetailsService.loadUserByUsername( username );
        String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }
}
关键代码就是这些,其他类代码参照后面提供的源码地址。
验证
登录,获取token
curl -X POST -d "username=admin&password=123456" http://127.0.0.1:8080/auth/login
返回
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU1NDQ1MzUwMX0.sglVeqnDGUL9pH1oP3Lh9XrdzJIS42VKBApd2nPJt7e1TKhCEY7AUfIXnzG9vc885_jTq4-h8R6YCtRRJzl8fQ
不带token访问资源
curl -X POST -d "name=zhangsan" http://127.0.0.1:8080/admin/hi
返回,拒绝访问
{
    "timestamp": "2019-03-31T08:50:55.894+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Access Denied",
    "path": "/auth/login"
}
携带token访问资源
curl -X POST -H "Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU1NDQ1MzUwMX0.sglVeqnDGUL9pH1oP3Lh9XrdzJIS42VKBApd2nPJt7e1TKhCEY7AUfIXnzG9vc885_jTq4-h8R6YCtRRJzl8fQ" -d "name=zhangsan" http://127.0.0.1:8080/admin/hi
返回正确
hi zhangsan , you have 'admin' role
源码
https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-jwt
推荐阅读
Spring Boot 系列精选
Spring Boot 自定义 starter
Spring Boot 整合 mybatis-plus
Spring Boot 整合 spring cache
Spring Boot 整合 rabbitmq
Spring Boot 整合 elasticsearch
Spring Boot 整合 docker
Spring Boot 整合 elk
Spring Boot Admin 2.0 详解
Spring Boot 整合 apollo
Spring Boot Security 详解
Spring Boot Security 整合 OAuth2 设计安全API接口服务
Spring Boot Security 整合 JWT 实现 无状态的分布式API接口
SpringCloud 系列精选
Spring Cloud Gateway 入门
Spring Cloud Gateway 之 Predict
Spring Cloud Gateway 之 Filter
Spring Cloud Gateway 之 限流
Spring Cloud Gateway 之 服务注册与发现
Dubbo 系列精选
Dubbo 搭建管理控制台
Dubbo 创建 提供者 消费者
Dubbo 配置
Dubbo 高可用
Docker 系列精选
服务 Docker 化
Docker 私有仓库搭建
DockerSwarm 集群环境搭建
DockerSwarm 微服务部署
欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~

Spring Boot Security 整合 JWT 实现 无状态的分布式API接口的更多相关文章
- Spring Boot Security 整合 OAuth2 设计安全API接口服务
		
简介 OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版.本文重点讲解Spring Boot项目对OAuth2进行的实现,如果你对OAut ...
 - Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器
		
概要 之前的两篇文章,讲述了Spring Security 结合 OAuth2 .JWT 的使用,这一节要求对 OAuth2.JWT 有了解,若不清楚,先移步到下面两篇提前了解下. Spring Bo ...
 - Spring Boot Security JWT 整合实现前后端分离认证示例
		
前面两章节我们介绍了 Spring Boot Security 快速入门 和 Spring Boot JWT 快速入门,本章节使用 JWT 和 Spring Boot Security 构件一个前后端 ...
 - spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)
		
一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...
 - Spring Boot:整合Spring Security
		
综合概述 Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架.除了常规的认证(Authentication)和授权(Author ...
 - Spring Security整合JWT,实现单点登录,So Easy~!
		
前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况 ...
 - 实战!spring Boot security+JWT 前后端分离架构认证登录!
		
大家好,我是不才陈某~ 认证.授权是实战项目中必不可少的部分,而Spring Security则将作为首选安全组件,因此陈某新开了 <Spring Security 进阶> 这个专栏,写一 ...
 - Spring Boot Security Oauth2之客户端模式及密码模式实现
		
Spring Boot Security Oauth2之客户端模式及密码模式实现 示例主要内容 1.多认证模式(密码模式.客户端模式) 2.token存到redis支持 3.资源保护 4.密码模式用户 ...
 - Spring Boot 应用系列 5 -- Spring Boot 2 整合logback
		
上一篇我们梳理了Spring Boot 2 整合log4j2的配置过程,其中讲到了Spring Boot 2原装适配logback,并且在非异步环境下logback和log4j2的性能差别不大,所以对 ...
 
随机推荐
- Java中ArrayList学习笔记
			
1. 先看两段代码 这段代码在执行的时候会报 但是这样写就好着呢: 总结,研究报错的代码 ,在for循环的时候调用next()方法,next方法中调用了checkForComodification这个 ...
 - RabbitMQ和Kafka到底怎么选(二)?
			
前言 前一篇文章<RabbitMQ和Kafka到底怎么选?>,我们在吞吐量方面比较了Kafka和RabbitMQ,知道了Kafka的吞吐量要高于RabbitMQ.本文从可靠性方面继续探讨两 ...
 - ASP.NET Core 实战:构建带有版本控制的 API 接口
			
一.前言 在上一篇的文章中,主要是搭建了我们的开发环境,同时创建了我们的项目模板框架.在整个前后端分离的项目中,后端的 API 接口至关重要,它是前端与后端之间进行沟通的媒介,如何构建一个 “好用” ...
 - Git学习:如何在Github的README.MD文件下添加图片
			
格式如下:  关于图片的绝对路径: 必须把图片上传到github的代码仓库里,再将其图片的网址复制到括号里才可以,不能够直接把图片复制到readme.md文件里面,这 ...
 - Android studio无法创建类和接口问题解决办法。提示 Unable to parse template "Class"
			
重新配置了studio 的工作环境, 没问题,后来加需求要新增java类和接口,这就出现问题了,新建的时候,就会报错: Unable to parse template "Interface ...
 - Api管家系列(一):初探
			
前段时间发现一个很好用的API管理工具--API管家,用了一段时间,已经感觉离不开了,抱着分享使我快乐的想法,因为刚开始用的时候随便写过一篇简介,不是很详细,所以现在就重新写,把我这段时间使用的经验和 ...
 - echarts切换tab时,第一个图表显示,第二个图表不显示的解决办法
			
首先这种情况,我在网上看到了好多人问,确实也有正解,但是只是说出了其错误出现的原因,并没有给出解决方法. 我也是深受其害,所以呢,在这里记录一下,分享给还在困扰着的你们,迅速找出解决办法. 首先,我先 ...
 - 多线程工具类:CountDownLatch、CyclicBarrier、Semaphore、LockSupport
			
◆CountDownLatch◆ 假如有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以.比如你想要买套房子,但是呢你现在手上没有钱.你得等这个月工资发了.然后年终奖发了.然后朋友借你得钱 ...
 - Python-图像处理库PIL图像变换transpose和transforms函数
			
1.transpose有这么几种模式FLIP_LEFT_RIGHT ,FLIP_TOP_BOTTOM ,ROTATE_90 ,ROTATE_180 ,ROTATE_270,TRANSPOSE ,TRA ...
 - 机器学习中如何处理不平衡数据(imbalanced data)?
			
推荐一篇英文的博客: 8 Tactics to Combat Imbalanced Classes in Your Machine Learning Dataset 1.不平衡数据集带来的影响 一个不 ...