SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘

Posted VipSoft

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘相关的知识,希望对你有一定的参考价值。

SpringBoot 集成 SpringSecurity + MySQL + JWT 无太多理论,直接盘
一般用于Web管理系统
可以先看 SpringBoot SpringSecurity 基于内存的使用介绍
本文介绍如何整合 SpringSecurity + MySQL + JWT

数据结构

数据库脚本:https://gitee.com/VipSoft/VipBoot/blob/develop/vipsoft-security/sql/Security.sql

常规权限管理数据结构设计,三张常规表:用户、角色、菜单,通过用户和角色的关系,角色和菜单(权限)的关系,实现用户和菜单(按钮)的访问控制权

用户登录

  1. SecurityConfig 中添加登录接口匿名访问配置

    .antMatchers("/auth/login", "/captchaImage").anonymous()
    BCryptPasswordEncoder 密码加密方式

  2. POST 登录接口 /auth/login

    调用 AuthorizationController.login 用户登录接口
    做入参、图形验证码等验证。

  3. 实现 UserDetailsService 接口

    根据用户名,去数据库获取用户信息、权限获取等

  4. 密码验证

    AuthorizationService.login
    调用 authenticationManager.authenticate(authenticationToken) 看密码是否正确
    可以在此集合 Redis 做失败次数逻辑处理

  5. 通过JWT 生成 Token

    调用 jwtUtil.generateToken(userId) 生成Token令牌
    将 用户信息放入 Redis
    剔除其它已登录的用户(如果需要)

  6. 返回Map对象给前端

接口权限认证

  1. 获取request.getHeader中的token信息

    AuthenticationTokenFilter.doFilterInternal
    解析 Token 中的用户ID 去 Redis 缓存中获取用户信息
    将信息赋到 SecurityContextHolder.getContext().setAuthentication(authenticationToken) 中,供权限验证获取用户信息使用, SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文

  2. 接口权限配置

    UserController 类的方法上,加了 @PreAuthorize("@ps.hasAnyPermi(\'system:user:list\')") 用来做权限控制

  3. 访问权限控制

    PermissionService.hasAnyPermi 判断,用户所拥有的权限,是否包含 @PreAuthorize("@ps.hasAnyPermi(\'system:user:list\')") 中配置的权限,包含则有权访问

用户登录代码

SecurityConfig

package com.vipsoft.web.config;

import com.vipsoft.web.security.AuthenticationEntryPointImpl;
import com.vipsoft.web.security.AuthenticationTokenFilter;
import com.vipsoft.web.security.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;


    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;
    /**
     * token认证过滤器
     */
    @Autowired
    private AuthenticationTokenFilter authenticationTokenFilter;



    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    /**
     * 强散列哈希加密实现
     * 必须 Bean 的形式实例化,否则会报 :Encoded password does not look like BCrypt
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    
        return new BCryptPasswordEncoder();
    

    /**
     * 配置用户身份的configure()方法
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    

    /**
     * 配置用户权限的configure()方法
     *
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception 
        httpSecurity
                // 禁用 CSRF,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers("/auth/login", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/webSocket/**"
                ).permitAll()
                // swagger 文档
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()
                .antMatchers("/druid/**").permitAll()
                // 放行OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 所有请求都需要认证
                .anyRequest().authenticated()
                //        .and().apply(this.securityConfigurerAdapter());

                .and()
                //设置跨域, 如果不设置, 即使配置了filter, 也不会生效
                .cors()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    


AuthenticationController.login

public Map<String, Object> login(SysUser user) 
    String username = user.getUserName();
    String password = user.getPassword();
    Authentication authentication;
    try 
        //该方法会去调用UserDetailsServiceImpl.loadUserByUsername
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        authentication = authenticationManager.authenticate(authenticationToken);
     catch (AuthenticationException ex) 
        Long incr = 3L; // Redis 实现
        if (incr > 5) 
            logger.error(" 账户连续次登录失败,账户被锁定30分钟", username, incr);
            throw new LockedException("密码连续输入错误次数过多,账户已被锁定!");
        
        throw new BadCredentialsException("您输入的用户名、密码或验证码不正确,为保证账户安全,连续5次输入错误,系统将锁定您的账户30分钟,当前剩余:" + (PASSOWRD_MAX_ERROR_COUNT - incr) + "次", ex);
    

    SecurityContextHolder.getContext().setAuthentication(authentication);
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();

    String userId = loginUser.getUser().getUserId().toString();
    // 生成令牌
    String token = jwtUtil.generateToken(userId);
    Map<String, Object> resultMap = new HashMap();
    resultMap.put("AccessToken", token);
    resultMap.put("UserId", userId);

    // Redis 保存上线信息
    // UserAgent userAgent
    // 踢掉已登录用户

    return resultMap;

UserDetailsServiceImpl


@Service
public class UserDetailsServiceImpl implements UserDetailsService 

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISysUserService userService;

    @Autowired
    private ISysMenuService menuService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        SysUser user = userService.selectUserByUserName(username);
        if (user == null) 
            logger.info("登录用户: 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
         else if ("1".equals(user.getDelFlag())) 
            logger.info("登录用户: 已被删除.", username);
            throw new CustomException("对不起,您的账号:" + username + " 已被删除");
         else if ("1".equals(user.getStatus())) 
            logger.info("登录用户: 已被停用.", username);
            throw new CustomException("对不起,您的账号:" + username + " 已停用");
        

        Set<String> perms = new HashSet<>();
        // 管理员拥有所有权限
        if (user.isAdmin()) 
            perms.add("*:*:*");
         else 
            perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
        
        return new LoginUser(user, perms);
    

接口权限认证代码



AuthenticationTokenFilter

@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter 
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException 
        LoginUser loginUser = jwtUtil.getLoginUser(request);
        if (loginUser != null && SecurityUtils.getAuthentication() == null) 
            jwtUtil.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            //SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文,确保PermissionService判断权限时可以获得当前LoginUser信息
            SecurityUtils.setAuthentication(authenticationToken);
        
        chain.doFilter(request, response);
    

定义权限验证类 PermissionService

package com.vipsoft.web.security;

import cn.hutool.core.util.StrUtil;
import com.vipsoft.web.utils.SecurityUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.Set;

/**
 * 自定义权限实现
 */
@Service("ps")
public class PermissionService 
    /**
     * 所有权限标识
     */
    private static final String ALL_PERMISSION = "*:*:*";

    /**
     * 管理员角色权限标识
     */
    private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMETER = ",";

    private static final String PERMISSION_DELIMETER = ",";


    /**
     * 对用户请求的接口进行验证,看接口所需要的权限,当前用户是否包括
     *
     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表,如:system:user:add,system:user:edit
     * @return 用户是否具有以下任意一个权限
     */
    public boolean hasAnyPermi(String permissions) 
        if (StrUtil.isEmpty(permissions)) 
            return false;
        
        LoginUser loginUser = SecurityUtils.getCurrentUser(); //去SecurityContextHolder.getContext()中获取登录用户信息
        if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) 
            return false;
        
        Set<String> authorities = loginUser.getPermissions();
        String[] perms = permissions.split(PERMISSION_DELIMETER);
        boolean hasPerms = Arrays.stream(perms).anyMatch(authorities::contains);
        //是Admin权限 或者 拥有接口所需权限时
        return permissions.contains(ALL_PERMISSION) || hasPerms;
    


详细代码见:https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-security
源代码摘自:若依后台管理系统

SpringBoot.03.SpringBoot集成jsp

SpringBoot.03.SpringBoot集成jsp

前言

在SpringBoot中默认推荐使用的模板引擎是Thymeleaf,但是作为传统Web开发的王者jsp在现在的一些企业中仍然被广泛使用。所以对于在SpringBoot中如何使用jsp还是需要有所了解。

现在广泛被推崇的是前后端分离,大有替代传统web开发的趋势。而前后端分离模式中VUE对于前端至关重要。所以对于VUE的学习也是必须的。

准备工作

由于本次演示SpringBoot.03.SpringBoot集成jsp模板,我们需要jsp页面。这里我们来新建一个jsp模板。

依次选择File->settings->Editor->File and Code Templates,然后选中Files;在nameExtension中键入jsp,将以下内容填到文本框中

<%@ page pageEncoding="UTF-8" language="java" contentType="text/html; UTF-8" %>
<!doctype html>
<html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>这是jsp页面的标题</title>
  </head>
  
  <body>
      <h1>Hello jsp!</h1>
  </body>
</html>

最后点击Apply->OK;如下图所示:

jsp集成案例

集成步骤

1.新建Module

我们选择Spring Initializr的方式新建一个Module,按照下图填写信息后点击Next

Dependencies中选择Spring Web,点击Finish,如下图所示:

2.pom.xml

<?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.5.4</version>
        <relativePath/>
    </parent>

    <groupId>com.christy</groupId>
    <artifactId>springboot-03-jsp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-03-jsp</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <!-- spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <!-- scope为test说明该依赖只在测试时有用,打包时不会打进来 -->
            <scope>test</scope>
        </dependency>

        <!-- c标签库 -->
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- 让内嵌tomcat具有解析jsp功能 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- spring-boot-maven-plugin 该插件是SpringBoot官方集成的maven插件,
            除了能保证正确执行java -jar来启动项目
            另外还能是项目正常加载jsp页面
            -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.Springboot03JspApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Springboot03JspApplication 

    public static void main(String[] args) 
        SpringApplication.run(Springboot03JspApplication.class, args);
    

4.application.yml

server:
  port: 8803

# 配置视图解析器
spring:
  mvc:
    view:
      prefix: /
      suffix: .jsp

5.index.jsp

我们在webapp目录右键选择新建index.jsp页面,如下图所示:

修改jsp页面内容如下:

<%@ page pageEncoding="UTF-8" language="java" contentType="text/html; UTF-8" %>
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>这是jsp页面的标题</title>
    </head>

    <body>
        <h1>Hello SpringBoot!</h1>
    </body>
</html>

6.JspController.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @Author Christy
 * @Date 2021/9/2 10:02
 * @Controller 这里不能使用@RestController注解,否则无法跳转页面只能输出字符串
 **/
@Controller
@RequestMapping("jsp")
public class JspController 

    private static final Logger log = LoggerFactory.getLogger(JspController.class);

    @RequestMapping("hello")
    public String sayHello()
        log.info("Hello Spring Boot!");
        // 由于我们配置了视图解析器,返回的index会被重定向为url地址:http://localhost:8803/index.jsp
        return "index";
    

7.测试

我们启动项目,浏览器访问http://localhost:8803/jsp/hello,结果如下:

一脸懵逼有没有?按理说配置啥的都没有错啊!为啥会404呢?

问题分析

之所以出现404原因就是项目并没有找到webapp这个目录,至于更深层次的原因我也不知道。如果哪位大牛能解释这个问题麻烦告知一下。

解决方案

1.springboot:run

在pom.xml文件中我们集成另外SpringBoot官方的maven插件,那么在右侧边栏maven中找到Plugins下的springboot:run命令运行项目。如下图所示:

项目重启后浏览器访问http://localhost:8803/jsp/hello,结果如下:

2.设置Working directory

我们选择Edit Configuration,在Working directory中选择MODULE_DIR。如下图所示:

项目再次重启后浏览器访问http://localhost:8803/jsp/hello,结果如下:

如果出现访问页面是变成了页面下载,说明jsp页面没有被正确编译和解析。请刷新一下maven重新加载一下依赖;

修改jsp无须重启应用

在实际开发过程中页面调试是非常繁琐的,在更改了相应程序之后需要同步刷新页面数据,这就需要频繁的重启应用。对于jsp的集成,SpringBoot提供了一种方法在更改程序或者数据后不需要重启应用,只需要重新刷新页面就可以了。

我们打开配置文件,加上以下代码,内容如下:

server:
  port: 8803
  servlet:
    jsp:
      init-parameters:
        development: true

# 配置视图解析器
spring:
  mvc:
    view:
      prefix: /     #这个代表jsp的根路径 需要在java包下新建webapp目录
      suffix: .jsp

我们重启应用,访问http://localhost:8803/jsp/hello,页面还是显示Hello SpringBoot!,然后我们在jsp页面中新增<h1>Hello Christy!</h1>,然后刷新页面。结果如下:

以上是关于SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot+Shiro+Jwt实现登录认证——最干的干货

Activiti7工作流引擎:进阶篇 SpringBoot整合工作流Activiti7

001.camunda入门(springboot集成篇)

使用 laravel 保护 javascript 资源

springboot集成ES,以及应用

SpringBoot集成Kafka