手把手带你搭建一个基于SpringSecurity前后端分离的登录模块

技术栈

SpringBoot  SpringSecurity 

数据库表

创建一个简单的RBAC模型,通过用户角色来控制权限

用户表

tb_user
idusernamepasswordrole_id
1admin{noop}admin1

 角色表

tb_role
idrole
1ROLE_ADMIN

主要代码 

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>security-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <artifactId>spring-boot-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.4.5</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.12</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.10.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
    </dependencies>

</project>

配置文件

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
    username: root
    password: 123456
mybatis:
  mapper-locations: classpath:mapper/*.xml   # mybatis映射文件路径
  type-aliases-package: com.audaque.security.entity # mybatis实体类别名

编写mapper文件

一个用户表,通过用户名查询,一个角色表,通过id查询

修改SpringSecurity认证逻辑

修改的方法有很多,这里选择重写Username Password AuthenticationFilter中的attemptAuthentication方法并创建UserDetailsService的实现类。登陆成功后这里求简选择直接将token存入session,可以将对应逻辑改为存入redis或数据库(登陆成功处理逻辑在源码)。

Username Password AuthenticationFilter子类

package com.audaque.security.filter;

import com.alibaba.fastjson2.JSON;
import com.audaque.security.entity.Constant;
import com.audaque.security.entity.DataResponse;
import com.audaque.security.util.ResponseUtil;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;

public class LoginFilter extends UsernamePasswordAuthenticationFilter implements Constant {

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { // 前后端分离,json格式传送数据
            BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
            StringBuilder sb = new StringBuilder();
            while (br.ready()) {
                sb.append(br.readLine());
            }
            // 解析Json
            Map<String,String> userMap = (Map<String, String>) JSON.parse(sb.toString());
            // getUsernameParameter() 默认调用父类的,默认值为"username"
            String username = userMap.get(getUsernameParameter());
            username = username != null ? username : "";
            username = username.trim();
            // getPasswordParameter() 默认调用父类的,默认值为"password"
            String password = userMap.get(getPasswordParameter());
            password = password != null ? password : "";
            // 照抄源码,只修改获取值得部分
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }else {
            ResponseUtil.writeJson(response,new DataResponse<>(FAILURE,null));
            return null;
        }
    }
}

如果有需求添加验证码在此方法对request参数进行操作也可以处理 

UserDetailsService实现类

package com.audaque.security.service;

import com.audaque.security.entity.LoginUser;
import com.audaque.security.entity.User;
import net.sf.ehcache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache;
import org.springframework.stereotype.Service;

@Service
public class UserDerailsServiceImpl implements UserDetailsService {

    // 添加ehcache缓存
    private EhCacheBasedUserCache userCache = new EhCacheBasedUserCache();

    private final UserService userService;

    private final CacheManager cacheManager;

    private final RoleService roleService;

    @Autowired
    public UserDerailsServiceImpl(UserService userService, CacheManager cacheManager, RoleService roleService) {
        this.cacheManager = cacheManager;
        this.roleService = roleService;
        this.userCache.setCache(this.cacheManager.getCache("userCache"));
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetails userFromCache = userCache.getUserFromCache(username);
        // 先从缓存中查找
        if (userFromCache != null) {
            return userFromCache;
        }else {
            User user = userService.selectUserByUsername(username);
            if (user == null) {
                throw new RuntimeException("用户名或密码错误");
            } else {
                LoginUser loginUser = new LoginUser(user, roleService.selectRoleById(user.getRoleId()));
                userCache.putUserInCache(loginUser);
                return loginUser;
            }
        }
    }
}

 为了提高登录效率。这里使用ehcache缓存已经登陆过的用户的信息,默认key为当前登录的用户名,可以自行查看源码中的putUserInCache()方法。

ehcache缓存配置文件,放到resources目录下即可

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <!-- 磁盘缓存位置 -->
    <diskStore path="c:/data/ehcache"/>

    <!-- 默认缓存 -->
    <!--
       name:缓存名称。
       maxElementsInMemory:缓存最大个数。
       eternal:对象是否永久有效,一但设置了,timeout将不起作用。
       timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。
                             仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
       timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。
                             仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
       overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
       diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
       maxElementsOnDisk:硬盘最大缓存个数。
       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
       memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。
                                     默认策略是LRU(最近最少使用)。
                                     你可以设置为FIFO(先进先出)
                                     或是LFU(较少使用)。
       clearOnFlush:内存数量最大时是否清除。
    -->
    <defaultCache

            maxElementsInMemory="10"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            overflowToDisk="true"
            memoryStoreEvictionPolicy="LRU"/>

    <cache name="userCache"
           maxElementsInMemory="100"
           eternal="false"
           timeToIdleSeconds="86400"
           overflowToDisk="true"
           memoryStoreEvictionPolicy="LRU"/>

</ehcache>

UserDetailServiceImpl返回值 

package com.audaque.security.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private Role role;

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> list = new ArrayList<>();
        list.add(new SimpleGrantedAuthority(role.getRole()));
        return list;
    }

    @Override
    @JsonIgnore // 如果登陆成功想要将当前用户写入redis进行此操作忽略该字段的序列化
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    @JsonIgnore
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return true;
    }
}

Security配置文件

package com.audaque.security.config;


import com.audaque.security.entity.Constant;
import com.audaque.security.entity.DataResponse;
import com.audaque.security.filter.LoginFilter;
import com.audaque.security.filter.TokenFilter;
import com.audaque.security.service.UserDerailsServiceImpl;
import com.audaque.security.util.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.ehcache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限控制 和@PreAuthorize,@PostAuthorize
public class SecurityConfig extends WebSecurityConfigurerAdapter implements Constant {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setUsernameParameter("username"); // 表单用户名input框name属性
        loginFilter.setPasswordParameter("password"); // 表单密码input框name属性
        loginFilter.setFilterProcessesUrl("/login");
        loginFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
        loginFilter.setAuthenticationFailureHandler((req,resp,e)->{  // 登陆失败处理,实现实现AuthenticateFailureHandler接口
            log.info("认证失败");
            // 由于该接口只有一个函数式抽象方法,可以写为lambda表达式
            ResponseUtil.writeJson(resp,new DataResponse<>(FAILURE,null));
        });
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        return loginFilter;
    }

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

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


//    @Bean
//    public UserDetailsService userDetailsService() {
//        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
//        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
//        return inMemoryUserDetailsManager;
//    }



    // 说明,登录=认证
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .mvcMatchers("/logout","/login","/")
                .permitAll()// 代表可以不认证即可访问"/logout","/login"
                .anyRequest().authenticated() // 剩下所有都需要认证才能访问
                .and()
                .formLogin()// 表示表单登录
//                .loginProcessingUrl("/login")// 登录提交的路径(security处理登录提交的路径/login,类似于@PostMapping("/login"))
//                .successHandler(new LoginSuccessHandler())// 登录成功处理 实现AuthenticateSuccessHandler
                                                        // 也可以写为lambda表达式 如下面登陆失败逻辑
//                .failureHandler((req,resp,e)->{  // 登陆失败处理,实现实现AuthenticateFailureHandler接口
                                                // 由于该接口只有一个函数式抽象方法,可以写为lambda表达式
//                    ResponseUtil.writeJson(resp,new DataResponse<>(FAILURE,null));
//                })
                .and()
                .logout()
                .logoutUrl("/logout") // 指定登出路径
                .logoutSuccessHandler((req,resp,auth)->{ // 退出登录成功逻辑
                    log.info("退出登录成功");
                    SecurityContextHolder.clearContext(); // 清空SecurityContext
                    ResponseUtil.writeJson(resp,new DataResponse<>(SUCCESS,null));
                })
                .and()
                .exceptionHandling()
                .accessDeniedHandler((req,resp,auth)->{ // 权限异常处理
                    log.info("权限不足");
                    ResponseUtil.writeJson(resp,new DataResponse<>(FAILURE,null));
                })
                .authenticationEntryPoint((req,resp,auth)->{ // 认证异常处理
                    log.info("认证异常");
                    ResponseUtil.writeJson(resp,new DataResponse<>(FAILURE,null));
                })
                .and()
                .addFilterBefore(new TokenFilter(),UsernamePasswordAuthenticationFilter.class)//验证token过滤器
                .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class) // 替换security的UsernamePasswordAuthenticationFilter
                .csrf().disable();
    }



}

gitee源码地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
使用 Spring Boot 3 开发一个前后端分离的生产级系统需要以下步骤: 第一步:环境准备 1. 安装 Java 开发工具包(JDK) 2. 安装集成开发环境(IDE),如Eclipse或IntelliJ IDEA 3. 安装Maven构建工具 4. 安装数据库(如MySQL)和相关工具(如MySQL Workbench) 第二步:创建后端项目 1. 使用IDE创建一个新的Spring Boot项目 2. 配置项目的基本信息,如项目名称、包名等 3. 添加必要的依赖,如Spring Boot Starter Web、Spring Data JPA等 4. 定义实体类、控制器、服务等后端代码 第三步:创建前端项目 1. 使用前端开发工具,如Vue.js或React.js,创建一个新的前端项目 2. 配置项目的基本信息,如项目名称、包名等 3. 定义前端路由、页面、组件等前端代码 第四步:前后端集成 1. 在后端项目中配置跨域访问,允许前端项目访问后端接口 2. 在前端项目中调用后端接口,实现数据的交互 第五步:开发和测试 1. 根据需求逐步开发后端和前端功能模块 2. 使用测试框架,如JUnit和Selenium,对系统进行单元测试和端到端测试 第六步:部署和上线 1. 打包后端项目为可执行的JAR文件 2. 部署JAR文件到生产环境的服务器上 3. 配置服务器的环境变量、数据库连接等 4. 启动服务器,验证系统是否正常运行 通过以上步骤,我们可以完成一个使用Spring Boot 3开发的前后端分离的生产级系统。这种架构可以提高开发效率、降低系统耦合性,并且适合大型项目的开发和部署。同时,我们还可以根据实际需求,进一步优化系统性能、可维护性和安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

生活低手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值