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

前言

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

Springboot整合JWT+Spring Security实战

认证流程图:

Springboot整合JWT+Spring Security实现无状态认证授权_第1张图片

添加pom依赖



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.4.0
         
    
    com.zkc
    springboot-jwt-springsecurity
    0.0.1-SNAPSHOT
    springboot-jwt-springsecurity
    Demo project for Spring Boot

    
        1.8
        0.9.0
        1.2.55
        3.3.1
        1.1.21
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            mysql
            mysql-connector-java
            runtime
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
        
            io.jsonwebtoken
            jjwt
            ${jwt.version}
        

        
        
            org.springframework.boot
            spring-boot-starter-security
        

        
        
            com.alibaba
            druid-spring-boot-starter
            ${springboot.druid.starter.version}
        
        
            com.baomidou
            mybatis-plus-boot-starter
            ${mybatis-plus.version}
        

        
        
            com.alibaba
            fastjson
            ${fastjson.version}
        

        
        
            org.hibernate.validator
            hibernate-validator
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    



配置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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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 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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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

Springboot整合JWT+Spring Security实现无状态认证授权_第2张图片

模拟不带token请求接口

Springboot整合JWT+Spring Security实现无状态认证授权_第3张图片

模拟携带正确token请求接口

Springboot整合JWT+Spring Security实现无状态认证授权_第4张图片

模拟无权限访问接口

Springboot整合JWT+Spring Security实现无状态认证授权_第5张图片

我的专栏

  1. 设计模式
  2. 认证授权框架实战
  3. java进阶知识
  4. maven进阶知
  5. spring进阶知识

你可能感兴趣的:(认证授权框架实战,jwt,分布式)