目录
第一步:新建一个类CustomerUserDetails实现UserDetails接口
第二步:新建CustomerUserDetailsServiceImpl来实现UserDetailService接口
第四步:在Service层使用ProviderManager的authenticate()方法进行验证
SpringSecurity顾名思义是spring的一个安全框架。拥有认证和授权两大核心功能。

RBAC模型:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。
即一个用户属于多种角色、一个角色有多个权限

主体(subject) 访问资源的时候、通常由分为两种:基于角色控制访问、基于权限控制访问;
故建立五张表:用户表、权限表、角色表、用户角色表、角色权限表;

准备数据:
张三--->管理员、普通用户------>增删改查
李四---->普通用户----->查询
脚本参考文章末尾的传送门
jdk 17
springboot 2.7.0
maven 3.8.6
mysql 8.0.30
导入必要jar包:主要导入:boot-security的整合依赖,其他根据需要导入
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> dependency> <dependency> <groupId>com.baomidougroupId> <artifactId>mybatis-plus-boot-starterartifactId> <version>3.4.0version> dependency> <dependency> <groupId>org.apache.velocitygroupId> <artifactId>velocity-engine-coreartifactId> <version>2.3version> dependency> <dependency> <groupId>com.baomidougroupId> <artifactId>mybatis-plus-generatorartifactId> <version>3.4.0version> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-thymeleafartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-testartifactId> <scope>testscope> dependency> <dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <optional>trueoptional> dependency>
配置文件:application.yml
spring: mvc: pathmatch: matching-strategy: ant_path_matcher server: port: 8080 --- spring: datasource: url: jdbc:mysql://localhost:3308/boot_security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&AllowPublicKeyRetrieval=True username: root password: root --- mybatis-plus: mapper-locations: mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
随后使用代码生成工具生成项目结构、随后调整xml文件位置、以及适当删减、随后测试下生成的代码是否可用。主要看有对应三个实体类就可。
- @Autowired
- private TbUserService userService;
-
- @Test
- public void test01(){
- userService.list();
- }

静态资源准备:

导入security整合依赖后、启动项目访问任何接口,都会被直接被直接拦截,并转发到security提供的登录页面、也就是需要认证一下才能进入首页。默认的用户名是user、密码在控制台。

认证通过后才会访问到目标页面

目前使用的是Security给的默认用户名和生成的密码。 实际情况是使用tb_user获取真实的用户名和密码;在此之前先了解Security的过滤链;
List filterList = context.getBean(DefaultSecurityFilterChain.class).getFilters();
SpringSecurity的过滤链:一共有16个过滤器链

过滤器链的大概流程就是,用户请求过来、先检查用户名密码、没有错、则检查权限,若有对应权限、访问对应的接口、其中只要一步错,就给打回去;
spring: security: user: name: zs password: 123
实现所有UserDetails的抽象方法并将TbUser【登录对象】 作为CustomerUserDetails的属性。
@Data @NoArgsConstructor public class CustomerUserDetails implements UserDetails { TbUser user; Listpermissions; public CustomerUserDetails(TbUser user,Listpermissions) { this.user = user; this.permissions = permissions; } /** * 权限集合:包括了角色code、可访问权限code * @return */ @Override public Collection extends GrantedAuthority> getAuthorities() { Listauthorities = new ArrayList<>(); permissions.forEach(x->{ GrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(x); authorities.add(simpleGrantedAuthority); }); return authorities; // return permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } @Override public String getPassword() { return user.getPassWord(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
实现loadUserByUsername方法。
- @Override
- public UserDetails loadUserByUsername(String username){
- //1、根据用户名查询用户信息
- LambdaQueryWrapper
wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(TbUser::getUserName,username);
- TbUser user = userMapper.selectOne(wrapper);
- //如果查询不到数据就通过抛出异常来给出提示
- if(Objects.isNull(user)){
- throw new RuntimeException("用户名或密码错误");
- }
- // 2、查询角色及权限:
- List
authoritiesList = new ArrayList<>(); - List
authoritiesPerms = userMapper.selectPermission(user); - List
roleList = userMapper.selectRole(user); - // 若 数据库代表角色的code没有加 ROLE_ 作为前缀、需要手动加一下;
- List
authoritiesRoles = this.addPrefix(roleList); -
- authoritiesList.addAll(authoritiesPerms);
- authoritiesList.addAll(authoritiesRoles);
- return new CustomerUserDetails(user,authoritiesList);
- }
-
- /**
- * 给role_code 加上前缀
- * @param roleList
- * @return
- */
- private List
addPrefix(List roleList) { - StringBuilder prefix = new StringBuilder("ROLE_");
- roleList.forEach(role->{
- role.setRoleCode(prefix.append(role.getRoleCode()).toString());
- });
- return roleList.stream().map(TbRole::getRoleCode).collect(Collectors.toList());
- }
此时由于数据库中的密码是明文,登录时会报一个错。
There is no PasswordEncoder mapped for the id "null"
因为没有给密码加密:
此时要想继续登录
方式1【不推荐】:将数据库中明文前加{noop}即可
方式2:使用Security默认的加密的工具类BCryptPasswordEncoder将密码加密后存入数据库。再SecurityConfig配置类中注入BCryptPasswordEncoder的bean对象即可。加密方式会自动加盐;
- /**
- * @author Alex
- */
- @Configuration
- public class SecurityConfig{
-
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
-
- }
将密码字符串加密,调用encode()将密码加密。将加密后的字符串存入数据库;
@Test public void testPasswordEncoder1(){ String encode = securityConfig.passwordEncoder().encode("123"); System.err.println(encode); } @Test public void testPasswordEncoder(){ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode = passwordEncoder.encode("123"); System.err.println(encode); }当注入bean对象后,明文前加{noop}就不可用了。

简单提一下解密:
SpringSecurity提供了matches()方法来进行密码匹配,加密本身时不可逆的,解密的原理是将需要解密的字段统过相同的Hash函数得到的字符串到已加密的数据库中进行匹配。

- /**
- * 登录方法、登录成功跳转到首先、
- * 否则继续跳转登录页,并给出提示
- * @param username
- * @return
- */
- @PostMapping("/login")
- public Map
userLogin(String username, String password){ - TbUser loginUser = new TbUser();
- loginUser.setUserName(username);
- loginUser.setPassWord(password);
- return userService.userLogin(loginUser);
- }
- @Bean
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- http
- //关闭csrf
- .csrf().disable()
- .authorizeRequests()
- // 允许匿名访问的接口
- .antMatchers("/user/login").anonymous()
- .antMatchers("/toLogin").anonymous()
- // 访问toUserAdd接口需要admin角色
- .antMatchers("/toUserAdd").hasAnyRole("admin")
- // 访问toUserEdit接口需要edit权限
- // .antMatchers("/toUserEdit").hasAnyAuthority("edit")
- // .antMatchers("/toUserEdit").hasAuthority("edit")
- .antMatchers("/toUserList").hasAnyAuthority("list")
- // 除上面外的所有请求全部需要鉴权认证
- .anyRequest().authenticated();
-
- http.formLogin()
- // 访问登录页面接口
- .loginPage("/toLogin")
- // 执行登录方法接口
- .loginProcessingUrl("user/login");
-
-
-
- // http.logout().logoutUrl("/logout");
- return http.build();
- }
将封装的Authentication对象 存入SecurityContextHolder
@Override public MapuserLogin(TbUser loginUser, HttpSession session) { MapresponseMap = new HashMap<>(2); try { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassWord(),null); Authentication authenticate = authenticationManager.authenticate(token); // 存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticate); responseMap.put("code","0"); return responseMap; }catch (RuntimeException e){ responseMap.put("code","-1"); e.printStackTrace(); return responseMap; } }此处直接返回map,也可以封装一个返回结果集对象,然后对于Security的 Session Management相关的内容会在后续文章中更新。
前端监听表单提交后发送登录请求:
//登录请求 const url = "user/login"; $.post(url,data,function(response){ console.log(response.code); if(response.code==0){ layer.msg("登录成功",{icon:6,time:1000}, function () { window.location = '/'; }); }else { layer.msg("用户名或密码错误",{icon:5,anim:6}); $("#btn-login").removeAttr("disabled", "disabled").removeClass("layui-btn-disabled"); } });
到这里呢、自定义登录就完成了、看下登录后的跳转的首页

若需接口注解生效,配置类上加@EnableGlobalMethodSecurity注解,将需要用到的实现机制设置为true;
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled=true)
eg1:配置访问toUserEdit接口需要edit权限
.antMatchers("/toUserEdit").hasAuthority("edit")或者:
.antMatchers("/toUserEdit").hasAnyAuthority("edit")或者:
@Secured("edit")或者:
@PreAuthorize("hasAnyAuthority('edit')")或者
@PreAuthorize("hasAuthority('edit')")若访问该接口需要多个权限:
@PreAuthorize("hasAuthority('edit') and hasAuthority('add')")
eg2:配置访问toUserAdd接口需要admin角色
.antMatchers("/toUserAdd").hasAnyRole("admin");或者:
.antMatchers("/toUserAdd").hasRole("admin");唯一区别是hasAnyRole可以传入多个角色code;点进hasRole方法中看到一些
或者再接口上加注解:
@Secured("ROLE_admin")或者:
@Secured({"ROLE_admin"})或者:
@PreAuthorize("hasAnyRole('admin')")要同时满足多个角色的话可以这样加:
@PreAuthorize("hasRole('admin') AND hasRole('user')")

此时前端的页面时包括所有的的接口按钮的,需要对将不属于当前角色或者没有权限的接口异常,为了标签生效,后端需要引入:thymeleaf整合security的yongriyong'ri
- <dependency>
- <groupId>org.thymeleaf.extrasgroupId>
- <artifactId>thymeleaf-extras-springsecurity5artifactId>
- dependency>
配置类中加入:
- /**
- * 页面的sec:标签生效
- * @return
- */
- @Bean
- public SpringSecurityDialect springSecurityDialect(){
- return new SpringSecurityDialect();
- }
前端需要引入security的命名空间:
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
index.html改造后: 所提供的标签属性和配置类的用法一致;
- html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org"
- xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
- >
- <head>
- <meta charset="UTF-8">
- <title>首页title>
- head>
- <body>
- <h1>hello,SpringSecurity、我是<span sec:authentication="principal.username">span>h1>
-
- <div sec:authorize="!isAuthenticated()">
- <a class="item" th:href="@{/toLogin}">登录a>
- div>
-
-
- <div sec:authorize="isAuthenticated()">
- <h2>角色及权限:<span sec:authentication="principal.authorities">span>h2>
- div>
-
- <div sec:authorize="isAuthenticated()">
- <a class="item" th:href="@{/logout}">注销a>
- div>
-
- <br>
- <hr>
- <br>
-
- <h1>基于角色展示:h1>
- <div>
- <div sec:authorize="hasRole('admin')">
- <a th:href="@{/toUserAdd}"> 添加用户a>
- <a th:href="@{/toUserDel}"> 删除用户a>
- <a th:href="@{/toUserEdit}"> 修改用户a>
- div>
- <div sec:authorize="hasAnyRole('user')">
- <a th:href="@{/toUserList}"> 用户列表a>
- div>
-
- div>
-
- <br>
- <hr>
- <br>
- <h1>基于权限展示:h1>
- <div>
- <div sec:authorize="hasAnyAuthority('add')">
- <a th:href="@{/toUserAdd}"> 添加用户a>
- div>
- <div sec:authorize="hasAuthority('delete')">
- <a th:href="@{/toUserDel}"> 删除用户a>
- div>
- <div sec:authorize="hasAnyAuthority('edit','select')">
- <a th:href="@{/toUserEdit}"> 修改用户a>
- div>
- <div sec:authorize="hasAuthority('select')">
- <a th:href="@{/toUserList}"> 用户列表a>
- div>
- div>
-
-
- body>
- html>
实现效果:

提交表单:拿到密码后对传入的密码进行随机盐+哈希散列加密、然后将随机盐和加密后的字符串存入数据库用户表中、并初始化一些用户及权限就🆗了。
将用户信息存入SecurityContextHolder的上下文只在springboot单体项目中可以使用。
前后端分离的情况下可以将token封装为JWT存入缓存。从缓存中取到并解析字符串也可获取用户信息。
1、Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
解决:配置文件加上
debug: true2、No qualifying bean of type 'com.example.demo.mapper.TbUserMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
解决:启动类上加上mapperScan("com......")
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
1、springSecurity如何校验用户名、密码和权限?
答:通过一个登录请求的debug得到如下流程
发送一个登录请求----->UsernamePasswordAuthenticationFilter--->通过authenticate()方法认证----->loadUserUsername()方法获得UserDetails对象-->从该对象中拿到密码对比系统中的密码---->给UserDetails对象添加权限并设置到Authentication中,存入SecurityContentHolder中即可。 (完成一个cookie--session的闭环)2、 为什么从数据库中查到角色code需要加上前缀 "ROLE_"再封装到CustomerUserDetails对象中?
个人理解是为了区分权限和角色的编码。我们点进关于登录的主体是否包括某个角色的验证源码时,在对应的验证方法中可以找到答案。也可以自定义验证方式。
private String defaultRolePrefix = "ROLE_";
public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) { String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); if (grantedAuthorityDefaultsBeanNames.length == 1) { GrantedAuthorityDefaults grantedAuthorityDefaults = (GrantedAuthorityDefaults)context.getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class); this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); } else { this.rolePrefix = "ROLE_"; } this.REGISTRY = new ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry(context); }