Springboot整合JWT+Spring Security实现无状态认证授权

认证授权框架实战 专栏收录该内容
1 篇文章 0 订阅

前言

  目前主流的认证授权框架包括:Spring Security,Shiro,JWT,Oauth2等。各自都有自己的优缺点和适用场景,百度一下有很多,理论知识了解了,重点还是需要自己上手去实操一篇。
  今天和大家分享一个基于springboot整合JWT+Spring Security,实现无状态的认证授权。“无状态“顾名思义,就是不依赖web容器的session会话机制去管理用户的认证信息。
  优点也比较明显:
  1. 方便实现集群和分布式的认证服务
  2. 服务端省去了管理session会话对象的内存和性能开销
  3. 便于前后端分离开发模式

Springboot整合JWT+Spring Security实战

认证流程图:

在这里插入图片描述

添加pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zkc</groupId>
    <artifactId>springboot-jwt-springsecurity</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-jwt-springsecurity</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <jwt.version>0.9.0</jwt.version>
        <fastjson.version>1.2.55</fastjson.version>
        <mybatis-plus.version>3.3.1</mybatis-plus.version>
        <springboot.druid.starter.version>1.1.21</springboot.druid.starter.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>

        <!-- spring-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- 数据库配置 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${springboot.druid.starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- json -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <!-- 校验框架 -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置application.yml

# 数据库连接池配置
spring:
  datasource:
    druid:
      url: jdbc:mysql:///jwt_springsecurity?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
      db-type: com.alibaba.druid.pool.DruidDataSource
      driverClassName: com.mysql.cj.jdbc.Driver
      username: root
      password: root
      validationQuery: SELECT 1
      testWhileIdle: true
      #初始化大小,最小,最大
      initialSize: 10
      minIdle: 10
      maxActive: 50
      #配置获取连接等待超时的时间
      maxWait: 60000
      #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      #配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 1800000
      #打开PSCache,并且指定每个连接PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      remove-abandoned: true
      remove-abandoned-timeout: 1800
      log-abandoned: true

# 持久层框架配置
mybatis-plus:
  mapper-locations: classpath*:mybatis/**/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    jdbc-type-for-null: 'NULL'
    map-underscore-to-camel-case: true
  global-config:
    banner: false

# jwt认证配置
jwt:
  expire-interval: 1800
  authentication-key: zkcjwt1234bbt

自定义jwt的认证过滤器

package com.zkc.springbootjwtspringsecurity.config;

import com.zkc.springbootjwtspringsecurity.service.impl.SecurityServiceImpl;
import com.zkc.springbootjwtspringsecurity.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-04
 */
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final SecurityServiceImpl securityService;
    private final JwtTokenUtil jwtTokenUtil;
    private static final String JWT_PREFIX = "JWTTOKEN ";
    private static final String AUTHORIZATION_HEAD = "Authorization";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHead = request.getHeader(AUTHORIZATION_HEAD);
        if (authorizationHead != null && authorizationHead.startsWith(JWT_PREFIX)) {
            String token = authorizationHead.substring(JWT_PREFIX.length());
            String userName = jwtTokenUtil.getUsernameFromToken(token);
            // 用户名不等于空 并且未认证过 进行登录验证
            if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = securityService.loadUserByUsername(userName);
                // 验证token
                if (jwtTokenUtil.validateToken(token, userDetails)) {
                    // 验证通过 构建Secruity登录对象
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request, response);
    }

}

配置springsecurity,将jwt过滤器加入认证责任链

package com.zkc.springbootjwtspringsecurity.config;

import com.zkc.springbootjwtspringsecurity.service.impl.SecurityServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-04
 */
@Configuration
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final SecurityServiceImpl securityService;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final AuthenticationFailedStrategy authenticationFailedStrategy;
    private final AccessFailedStrategy accessFailedStrategy;

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/static/**", "/health", "/index.html", "/css/**", "/js/**", "/img/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors().and().csrf().disable()
                .addFilterBefore(jwtAuthenticationFilter, AnonymousAuthenticationFilter.class)
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .antMatchers("/api/login", "/api/register").permitAll())
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .anyRequest().authenticated()
                ).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(authenticationFailedStrategy).accessDeniedHandler(accessFailedStrategy);

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(securityService).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

自定义springsecurity的认证service

package com.zkc.springbootjwtspringsecurity.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.zkc.springbootjwtspringsecurity.dto.SecurityUser;
import com.zkc.springbootjwtspringsecurity.dto.UserDO;
import com.zkc.springbootjwtspringsecurity.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-04
 */
@Service
@RequiredArgsConstructor
public class SecurityServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(UserDO::getUserName, userName);
        UserDO userDO = userMapper.selectOne(queryWrapper);
        if (userDO != null) {
            return new SecurityUser(userName, userDO.getPassWord(), new ArrayList<>(), userDO.getEnabled());
        } else {
            throw new UsernameNotFoundException(String.format("%s 该账号不存在", userName));
        }
    }

}

自定义认证失败处理类

package com.zkc.springbootjwtspringsecurity.config;

import com.zkc.springbootjwtspringsecurity.enums.ResponseCodeEnum;
import com.zkc.springbootjwtspringsecurity.util.ResponseOut;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-09
 */
@Component
public class AuthenticationFailedStrategy implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseOut.out(response, ResponseCodeEnum.AUTHENTICATION_FAIL.getCode(), e.getMessage());
    }

}

自定义授权失败处理类

package com.zkc.springbootjwtspringsecurity.config;

import com.zkc.springbootjwtspringsecurity.enums.ResponseCodeEnum;
import com.zkc.springbootjwtspringsecurity.util.ResponseOut;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-09
 */
@Component
public class AccessFailedStrategy implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        ResponseOut.out(response, ResponseCodeEnum.ACCESS_FAIL.getCode(), e.getMessage());
    }

}

注册/登录处理类

package com.zkc.springbootjwtspringsecurity.service.impl;

import com.zkc.springbootjwtspringsecurity.dto.UserCO;
import com.zkc.springbootjwtspringsecurity.dto.UserDO;
import com.zkc.springbootjwtspringsecurity.enums.UserEnableEnum;
import com.zkc.springbootjwtspringsecurity.mapper.UserMapper;
import com.zkc.springbootjwtspringsecurity.service.LoginRegisterService;
import com.zkc.springbootjwtspringsecurity.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-10
 */
@Service
@RequiredArgsConstructor
public class LoginRegisterServiceImpl implements LoginRegisterService {

    private final UserMapper userMapper;
    private final AuthenticationManager authenticationManager;
    private final JwtTokenUtil jwtTokenUtil;


    @Override
    public boolean register(UserCO userCO) {
        UserDO userDO = new UserDO();
        userDO.setUserName(userCO.getUserName());
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        userDO.setPassWord(passwordEncoder.encode(userCO.getPassWord()));
        userDO.setCreateTime(LocalDateTime.now());
        userDO.setEnabled(UserEnableEnum.ENABLE.getCode());
        return userMapper.insert(userDO) == 1 ? true : false;
    }

    @Override
    public String login(String userName, String passWord) {
        // 创建Spring Security登录token
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, passWord);
        // 委托Spring Security认证组件执行认证过程
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        // 认证成功,设置上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 生成jwt token
        return jwtTokenUtil.generateToken(userName);
    }

}

验证Controller

package com.zkc.springbootjwtspringsecurity.controller;

import com.zkc.springbootjwtspringsecurity.dto.ResponseData;
import com.zkc.springbootjwtspringsecurity.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2020-12-10
 */
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class DemoController {

    private final UserService userService;

    @GetMapping("/userList")
    public ResponseData listUser(){
        return ResponseData.builderSuccess(userService.listUser());
    }

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/user/{uid}")
    public ResponseData getUserById(@PathVariable("uid") Long uid){
        return ResponseData.builderSuccess(userService.getUserById(uid));
    }

}

完整的代码上传在码云

码云Git地址:https://gitee.com/zhang_kaicheng/springboot-jwt-springsecurity.git
链接: clone地址

Postman验证结果

登录获取jwt token

在这里插入图片描述

模拟不带token请求接口

在这里插入图片描述

模拟携带正确token请求接口

在这里插入图片描述

模拟无权限访问接口

在这里插入图片描述

我的专栏

  1. 设计模式
  2. 认证授权框架实战
  3. java进阶知识
  4. maven进阶知
  5. spring进阶知识
  • 3
    点赞
  • 0
    评论
  • 7
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
<p> <span style="font-size:16px;"><br /> </span> </p> <p> <span style="font-size:16px;"><strong>课程简介:<br /> </strong>本课程主要是跟各位小伙伴分享、介绍并实战两大核心的用户身份认证(接口鉴权)模式,即</span><span style="font-size:16px;">基于</span><span style="font-size:16px;">Token</span><span style="font-size:16px;">的认证模式</span><span style="font-size:16px;"> 以及 </span><span style="font-size:16px;">基于</span><span style="font-size:16px;">Session</span><span style="font-size:16px;">的认证模式</span><span style="font-size:16px;">,其中</span><span></span> </p> <p> <span style="font-size:16px;">(1)   </span><span style="font-size:16px;">基于</span><span style="font-size:16px;">Token</span><span style="font-size:16px;">的认证模式</span><span style="font-size:16px;"> 则主要介绍了三种核心、主流的认证模式,即基于</span><span style="font-size:16px;">Token+</span><span style="font-size:16px;">数据库、基于</span><span style="font-size:16px;">Token+</span><span style="font-size:16px;">缓存中间件</span><span style="font-size:16px;">Redis</span><span style="font-size:16px;">、基于</span><span style="font-size:16px;">Token+JWT</span><span style="font-size:16px;">的认证模式。</span><span></span> </p> <p> <span style="font-size:16px;">(2)   </span><span style="font-size:16px;">基于</span><span style="font-size:16px;">Session</span><span style="font-size:16px;">的认证模式 </span><span style="font-size:16px;">也主要介绍了三种核心、主流的认证模式,即基于原生</span><span style="font-size:16px;">Spring Session</span><span style="font-size:16px;">以及</span><span style="font-size:16px;">Session</span><span style="font-size:16px;">共享的认证模式、基于</span><span style="font-size:16px;">Shiro Session</span><span style="font-size:16px;">的认证模式、基于</span><span style="font-size:16px;">Shiro + Redis </span><span style="font-size:16px;">的</span><span style="font-size:16px;">Session</span><span style="font-size:16px;">共享认证模式</span><span></span> </p> <p> <span style="font-size:16px;">即课程的整体介绍如下图所示:</span> </p> <p> <span style="font-size:16px;"><img src="https://img-bss.csdn.net/201909120730297517.png" alt="" /><br /> </span> </p> <p> <span style="font-size:16px;"> </span> </p> <p> 核心技术栈列表: </p> 值得介绍的是,本课程在技术栈层面涵盖了“用户身份认证”、“接口鉴权”等业务场景 常用的大部分技术,包括<span>Spring Boot2.x</span>、<span>Spring MVC</span>、<span>Mybatis</span>、加密解密算法<span>AES</span>、雪花算法<span>Snowflake</span>、统一验参工具<span>ValidatorUtil</span>、<span>JWT</span>(<span>Json Web Token</span>)、缓存中间件<span>Redis</span>、<span>Shiro(</span>身份认证与会话等等<span>)</span>、过滤器<span>Filter</span>、拦截器<span>Interceptor</span>、热部署插件<span>Devtools</span>、等等,如下图所示<br /> <p> <span style="font-size:16px;"><img src="https://img-bss.csdn.net/201909120732073201.png" alt="" /><br /> </span> </p> <p> <span style="font-size:16px;"> </span> </p> <p> <br /> </p> <p> 值得一提的是,本课程所介绍的核心重点在于“仅仅围绕基于<span>Token</span>的认证模式”进行展开讲解与实战,如下图所示为<span>Debug</span>亲自罗列、归纳出来的几大核心要点(面试官就经常喜欢这样面): </p> <img src="https://img-bss.csdn.net/201909120732381227.png" alt="" /><br /> <p> <span style="font-size:16px;"><br /> </span> </p> <p> <span style="font-size:16px;"> </span> </p> <p> 如下图所示为 基于<span>Token</span>认证模式 总体上的时序图:<span></span> </p> <img src="https://img-bss.csdn.net/201909120733009772.png" alt="" /><br /> <p> <br /> </p>
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值