Spring Security入门教程:实现用户的授权功能
上一篇文章我们讲了怎么去实现自定义用户配置,其实大家学习spring security的时候,他其实做的只有两件事,第一是认证,第二是授权。那么前面我们所学的两节课就是认证。但是他登陆之后,我们需要给定指定的资源去给他访问。给哪些资源?这就是授权。所以说我们这节课来学习一下。Spring Security的授权。
首先我们来进行一个简单的演示。新建一个用于演示的接口。
package com.masiyi.springsecuritydemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/admin") //ADMIN
public String admin() {
return "admin ok";
}
@GetMapping("/user") //USER
public String user() {
return "user ok";
}
@GetMapping("/getInfo") //READ_INFO
public String getInfo() {
return "info ok";
}
}
接着我们在配置类里面进行如下配置
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
return inMemoryUserDetailsManager;
}
如果登陆admin用户成功了,并且访问这个地址就会发现成功访问。
如果我们登录角色不是admin的用户就会显示这样的页面。返回403状态码。
但是我们刚刚编写的代码,它其实是在内存中创建对应的用户。但是实际的项目中我们肯定不会这么用的,我们肯定也会跟用户存储一样。把授权菜单角色给放入到数据库中。下一步我们去实现这个步骤。把这些放入数据库中去实现。
这里给大家准备了一个SQL脚本,大家直接执行就行。这个里面我们创建了菜单,角色,用户,还有他们之间的关系表并且给他们初始化数据。
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50738
Source Host : localhost:3306
Source Schema : springsecurity
Target Server Type : MySQL
Target Server Version : 50738
File Encoding : 65001
Date: 22/02/2024 21:47:51
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, '/admin/**');
INSERT INTO `menu` VALUES (2, '/user/**');
INSERT INTO `menu` VALUES (3, '/guest/**');
-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `mid`(`mid`) USING BTREE,
INDEX `rid`(`rid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES (1, 1, 1);
INSERT INTO `menu_role` VALUES (2, 2, 2);
INSERT INTO `menu_role` VALUES (3, 3, 3);
INSERT INTO `menu_role` VALUES (4, 3, 2);
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`name_zh` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_ADMIN', '系统管理员');
INSERT INTO `role` VALUES (2, 'ROLE_USER', '普通用户');
INSERT INTO `role` VALUES (3, 'ROLE_GUEST', '游客');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`enabled` tinyint(1) DEFAULT 1,
`accountNonExpired` tinyint(1) DEFAULT 1,
`accountNonLocked` tinyint(1) DEFAULT 1,
`credentialsNonExpired` tinyint(1) DEFAULT 1,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (2, 'user', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (3, 'masiyi', '{noop}123', 1, 1, 1, 1);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `uid`(`uid`) USING BTREE,
INDEX `rid`(`rid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);
SET FOREIGN_KEY_CHECKS = 1;
创建表对应的实体类。
package com.masiyi.springsecuritydemo.entity;
import java.util.List;
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
}
package com.masiyi.springsecuritydemo.entity;
import java.io.Serializable;
public class Role implements Serializable {
private Integer id;
private String name;
private String nameZh;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
}
/*
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.masiyi.springsecuritydemo.entity;
public class User {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public User(Integer age, String name) {
this.name = name;
this.age = age;
}
public User() {
}
}
这里为了区分数据库对应的实体类跟spring security里面的userdetails,所以做了两个类去区分。
package com.masiyi.springsecuritydemo.entity;
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;
import java.util.List;
import java.util.Objects;
public class UserDetail implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserDetail userDetail = (UserDetail) o;
return Objects.equals(id, userDetail.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
return grantedAuthorities;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Boolean getAccountNonExpired() {
return accountNonExpired;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public Boolean getAccountNonLocked() {
return accountNonLocked;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public Boolean getCredentialsNonExpired() {
return credentialsNonExpired;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
创建对应的xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.masiyi.springsecuritydemo.dao.MenuDao">
<resultMap id="MenuResultMap" type="com.masiyi.springsecuritydemo.entity.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="com.masiyi.springsecuritydemo.entity.Role">
<id column="rid" property="id"/>
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getAllMenu" resultMap="MenuResultMap">
select m.*, r.id as rid, r.name as rname, r.name_zh as rnameZh
from menu m
left join menu_role mr on m.`id` = mr.`mid`
left join role r on r.`id` = mr.`rid`
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.masiyi.springsecuritydemo.dao.UserDao">
<insert id="insertUser">
insert into user(username,password) values (#{userInfo.username},#{userInfo.password})
</insert>
<!--查询单个-->
<select id="loadUserByUsername" resultType="com.masiyi.springsecuritydemo.entity.UserDetail">
select id,
username,
password,
enabled,
accountNonExpired,
accountNonLocked,
credentialsNonExpired
from user
where username = #{username}
</select>
<!--查询指定行数据-->
<select id="getRolesByUid" resultType="Role">
select r.id,
r.name,
r.name_zh nameZh
from role r,
user_role ur
where r.id = ur.rid
and ur.uid = #{uid}
</select>
<select id="getUserRoleByUid" resultType="com.masiyi.springsecuritydemo.entity.Role">
select r.*
from role r,
user_role ur
where ur.uid = #{uid}
and ur.rid = r.id
</select>
</mapper>
由于篇幅原因,这里就不把全部的代码给贴出来了。大家可以到我的代码仓库里面,把一些剩余的代码给拷贝过来。之后我们把配置类改写成这样。
package com.masiyi.springsecuritydemo.config;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationFailureHandler;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationSuccessHandler;
import com.masiyi.springsecuritydemo.handler.MyLogoutSuccessHandler;
import com.masiyi.springsecuritydemo.metasource.CustomerSecurityMetaSource;
import com.masiyi.springsecuritydemo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.config.annotation.web.configurers.UrlAuthorizationConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true, jsr250Enabled=true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private FindByIndexNameSessionRepository sessionRepository;
private CustomerSecurityMetaSource customSecurityMetadataSource;
@Autowired
@Lazy
public void SecurityConfig(FindByIndexNameSessionRepository sessionRepository,
CustomerSecurityMetaSource customSecurityMetadataSource) {
this.sessionRepository = sessionRepository;
this.customSecurityMetadataSource = customSecurityMetadataSource;
}
/**
* 这里有两种方式 authorizeHttpRequests 和 authorizeRequests
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
// @Override
// protected void configure(HttpSecurity http) throws Exception {
// http.authorizeHttpRequests()
// .mvcMatchers("/user/register").permitAll()
// .mvcMatchers(HttpMethod.GET,"/admin").hasRole("ADMIN") //具有 admin 角色 强大 通用: /admin /admin/ /admin.html
// .mvcMatchers("/user").hasRole("USER") //具有 user 角色
// .mvcMatchers("/getInfo").hasAuthority("READ_INFO") //READ_INFO 权限
// .antMatchers(HttpMethod.GET,"/admin").hasRole("ADMIN")
// .anyRequest().authenticated()
// .and().formLogin()
// .successHandler(new MyAuthenticationSuccessHandler())
// .failureHandler(new MyAuthenticationFailureHandler())
// .and()
// .logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
// .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// .and()
// .cors() //跨域处理方案
// .configurationSource(configurationSource())
// .and()
// .rememberMe() //开启记住我功能
// .and()
// .sessionManagement() //开启会话管理
// .maximumSessions(1) //设置会话并发数为 1 在SessionRegistryImpl中管理key,key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等
// ;
// }
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.获取工厂对象
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
//2.设置自定义 url 权限处理
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(customSecurityMetadataSource);
//是否拒绝公共资源访问
object.setRejectPublicInvocations(false);
return object;
}
});
http.formLogin().and().csrf().disable();
}
/**
*允许所有来源的请求(*)使用任意的请求头和请求方法,并且设置了最大缓存时间为 3600 秒。
* @return
*/
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
//
// @Bean
// public UserDetailsService userDetailsService() {
// InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
// inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
// inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
// inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
// return inMemoryUserDetailsManager;
// }
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public UserDetailsService userDetailsService() {
return new MyUserDetailService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
auth.userDetailsService(userDetailsService());
}
//
// /**
// * 指定加密方式
// */
// @Bean
// public PasswordEncoder passwordEncoder() {
// // 使用BCrypt加密密码
// return new BCryptPasswordEncoder();
// }
}
其中最重要的部分就是这一块的改变。通过这样写,我们就可以动态的获取数据库里面的信息。
这段代码是一个用于配置Spring Security的configure(HttpSecurity http)方法。在该方法中,首先通过http.getSharedObject(ApplicationContext.class)获取ApplicationContext对象,然后通过http.apply(new UrlAuthorizationConfigurer<>(applicationContext))设置自定义的URL权限处理。
在ObjectPostProcessor中,通过object.setSecurityMetadataSource(customSecurityMetadataSource)设置了自定义的安全元数据源customSecurityMetadataSource,用于动态获取资源的权限配置信息。另外,通过object.setRejectPublicInvocations(false)设置是否拒绝公共资源访问为false,允许公共资源的访问。好
最后,通过http.formLogin().and().csrf().disable()配置了表单登录和禁用CSRF保护。
这个里面我们新建了一个CustomerSecurityMetaSource 类。
package com.masiyi.springsecuritydemo.metasource;
import com.masiyi.springsecuritydemo.entity.Menu;
import com.masiyi.springsecuritydemo.entity.Role;
import com.masiyi.springsecuritydemo.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
@Component
public class CustomerSecurityMetaSource implements FilterInvocationSecurityMetadataSource {
private final MenuService menuService;
@Autowired
public CustomerSecurityMetaSource(MenuService menuService) {
this.menuService = menuService;
}
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 自定义动态资源权限元数据信息
* @param object the object being secured
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//1.当前请求对象
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
//2.查询所有菜单
List<Menu> allMenu = menuService.getAllMenu();
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getPattern(), requestURI)) {
String[] roles = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
这段代码是一个自定义的Spring Security权限元数据源(SecurityMetadataSource),用于动态获取资源的权限配置信息。在该类中,通过实现FilterInvocationSecurityMetadataSource接口,重写了getAttributes()方法,根据请求的URL动态获取对应的权限配置信息。
具体来说,该类通过注入MenuService来获取所有菜单信息,然后根据请求的URL匹配菜单的URL模式,从而确定该请求需要的权限配置。如果请求的URL匹配到了某个菜单的URL模式,就返回该菜单所需的角色权限;否则返回null。
通过这种方式,可以实现动态的资源权限配置,根据不同的URL动态确定需要的权限,从而实现细粒度的权限控制。
这样的话,一个实现用户的授权功能的基本框架就搭出来了。至于后面的菜单角色跟用户的真相改查。这里就不一一做描述。相信这些功能已经很简单了。大家可以根据项目中实际的情况进行增删改查功能的编写。至此Spring Security的入门教程这篇专栏就到这里就结束了,相信大家读了这几篇专栏之后,会对spring security有一个入门的了解。
项目的地址就在 gitee.com/WangFuGui-M… 如果大家对这篇文章或者专栏有兴趣或者对大家有所帮助的话,欢迎关注点赞。加评论。 我们spring security的进阶专栏见。
转载自:https://juejin.cn/post/7339155841182056474