• springboot整合SpringSecurity并实现简单权限控制


    目录

    一、SpringSecurity介绍

            案例效果:

    二、环境准备

            2.1 数据库

            2.2 项目准备    

    三、确保项目没问题后开始使用

    3.1、Security的过滤链:

    3.2、自定义用户名密码登录:

    方式1:将用户名密码写在配置文件里

    方式2:使用数据库中的用户名、密码进行登录:

            第一步:新建一个类CustomerUserDetails实现UserDetails接口

            第二步:新建CustomerUserDetailsServiceImpl来实现UserDetailService接口

            第三步:配置类中注入bean对象:

    3.3、自定义登录/认证:

            第一步:自定义登录页面

            第二步:定义一个登录接口

            第三步:放行登录接口、请求登录接口

            第四步:在Service层使用ProviderManager的authenticate()方法进行验证

            实现效果:

            3.4、后端接口拦截

                    1、在SecurityFilterChain中完成配置

                    2、在接口上加注解

            3.5、前端按钮隐藏

                    3.6、自定义注册

                  3.7、关于拓展

    过程中的一些报错

    认证过程:

    传送门:


    一、SpringSecurity介绍

      SpringSecurity顾名思义是spring的一个安全框架。拥有认证和授权两大核心功能。

            案例效果:

    二、环境准备

            2.1 数据库

    RBAC模型:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。

    即一个用户属于多种角色、一个角色有多个权限

     主体(subject) 访问资源的时候、通常由分为两种:基于角色控制访问、基于权限控制访问;

    故建立五张表:用户表、权限表、角色表、用户角色表、角色权限表;

    准备数据:

            张三--->管理员、普通用户------>增删改查 

           李四---->普通用户----->查询

    脚本参考文章末尾的传送门

            2.2 项目准备    

    jdk 17 

    springboot 2.7.0         

    maven 3.8.6      

    mysql 8.0.30

     导入必要jar包:主要导入:boot-security的整合依赖,其他根据需要导入

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-webartifactId>
    4. dependency>
    5. <dependency>
    6. <groupId>mysqlgroupId>
    7. <artifactId>mysql-connector-javaartifactId>
    8. dependency>
    9. <dependency>
    10. <groupId>com.baomidougroupId>
    11. <artifactId>mybatis-plus-boot-starterartifactId>
    12. <version>3.4.0version>
    13. dependency>
    14. <dependency>
    15. <groupId>org.apache.velocitygroupId>
    16. <artifactId>velocity-engine-coreartifactId>
    17. <version>2.3version>
    18. dependency>
    19. <dependency>
    20. <groupId>com.baomidougroupId>
    21. <artifactId>mybatis-plus-generatorartifactId>
    22. <version>3.4.0version>
    23. dependency>
    24. <dependency>
    25. <groupId>org.springframework.bootgroupId>
    26. <artifactId>spring-boot-starter-securityartifactId>
    27. dependency>
    28. <dependency>
    29. <groupId>org.springframework.bootgroupId>
    30. <artifactId>spring-boot-starter-thymeleafartifactId>
    31. dependency>
    32. <dependency>
    33. <groupId>org.springframework.bootgroupId>
    34. <artifactId>spring-boot-starter-testartifactId>
    35. <scope>testscope>
    36. dependency>
    37. <dependency>
    38. <groupId>org.projectlombokgroupId>
    39. <artifactId>lombokartifactId>
    40. <optional>trueoptional>
    41. dependency>

    配置文件:application.yml

    1. spring:
    2. mvc:
    3. pathmatch:
    4. matching-strategy: ant_path_matcher
    5. server:
    6. port: 8080
    7. ---
    8. spring:
    9. datasource:
    10. url: jdbc:mysql://localhost:3308/boot_security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&AllowPublicKeyRetrieval=True
    11. username: root
    12. password: root
    13. ---
    14. mybatis-plus:
    15. mapper-locations: mapper/*.xml
    16. configuration:
    17. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    18. map-underscore-to-camel-case: true

    随后使用代码生成工具生成项目结构、随后调整xml文件位置、以及适当删减、随后测试下生成的代码是否可用。主要看有对应三个实体类就可。

    1. @Autowired
    2. private TbUserService userService;
    3. @Test
    4. public void test01(){
    5. userService.list();
    6. }

    静态资源准备:

    三、确保项目没问题后开始使用

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

            

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

    3.1、Security的过滤链:

    目前使用的是Security给的默认用户名和生成的密码。 实际情况是使用tb_user获取真实的用户名和密码;在此之前先了解Security的过滤链;

    List filterList = context.getBean(DefaultSecurityFilterChain.class).getFilters();

    SpringSecurity的过滤链:一共有16个过滤器链

     过滤器链的大概流程就是,用户请求过来、先检查用户名密码、没有错、则检查权限,若有对应权限、访问对应的接口、其中只要一步错,就给打回去;

    3.2、自定义用户名密码登录:

    方式1:将用户名密码写在配置文件里

    1. spring:
    2. security:
    3. user:
    4. name: zs
    5. password: 123

    方式2:使用数据库中的用户名、密码进行登录:

            第一步:新建一个类CustomerUserDetails实现UserDetails接口

    实现所有UserDetails的抽象方法并将TbUser【登录对象】 作为CustomerUserDetails的属性。

            

    1. @Data
    2. @NoArgsConstructor
    3. public class CustomerUserDetails implements UserDetails {
    4. TbUser user;
    5. List permissions;
    6. public CustomerUserDetails(TbUser user,List permissions) {
    7. this.user = user;
    8. this.permissions = permissions;
    9. }
    10. /**
    11. * 权限集合:包括了角色code、可访问权限code
    12. * @return
    13. */
    14. @Override
    15. public Collectionextends GrantedAuthority> getAuthorities() {
    16. List authorities = new ArrayList<>();
    17. permissions.forEach(x->{
    18. GrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(x);
    19. authorities.add(simpleGrantedAuthority);
    20. });
    21. return authorities;
    22. // return permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    23. }
    24. @Override
    25. public String getPassword() {
    26. return user.getPassWord();
    27. }
    28. @Override
    29. public String getUsername() {
    30. return user.getUserName();
    31. }
    32. @Override
    33. public boolean isAccountNonExpired() {
    34. return true;
    35. }
    36. @Override
    37. public boolean isAccountNonLocked() {
    38. return true;
    39. }
    40. @Override
    41. public boolean isCredentialsNonExpired() {
    42. return true;
    43. }
    44. @Override
    45. public boolean isEnabled() {
    46. return true;
    47. }
    48. }

            第二步:新建CustomerUserDetailsServiceImpl来实现UserDetailService接口

            实现loadUserByUsername方法。

    1. @Override
    2. public UserDetails loadUserByUsername(String username){
    3. //1、根据用户名查询用户信息
    4. LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
    5. wrapper.eq(TbUser::getUserName,username);
    6. TbUser user = userMapper.selectOne(wrapper);
    7. //如果查询不到数据就通过抛出异常来给出提示
    8. if(Objects.isNull(user)){
    9. throw new RuntimeException("用户名或密码错误");
    10. }
    11. // 2、查询角色及权限:
    12. List authoritiesList = new ArrayList<>();
    13. List authoritiesPerms = userMapper.selectPermission(user);
    14. List roleList = userMapper.selectRole(user);
    15. // 若 数据库代表角色的code没有加 ROLE_ 作为前缀、需要手动加一下;
    16. List authoritiesRoles = this.addPrefix(roleList);
    17. authoritiesList.addAll(authoritiesPerms);
    18. authoritiesList.addAll(authoritiesRoles);
    19. return new CustomerUserDetails(user,authoritiesList);
    20. }
    21. /**
    22. * 给role_code 加上前缀
    23. * @param roleList
    24. * @return
    25. */
    26. private List addPrefix(List roleList) {
    27. StringBuilder prefix = new StringBuilder("ROLE_");
    28. roleList.forEach(role->{
    29. role.setRoleCode(prefix.append(role.getRoleCode()).toString());
    30. });
    31. return roleList.stream().map(TbRole::getRoleCode).collect(Collectors.toList());
    32. }

    此时由于数据库中的密码是明文,登录时会报一个错。

    There is no PasswordEncoder mapped for the id "null"

    因为没有给密码加密:

    此时要想继续登录

                    方式1【不推荐】:将数据库中明文前加{noop}即可

                    方式2:使用Security默认的加密的工具类BCryptPasswordEncoder将密码加密后存入数据库。再SecurityConfig配置类中注入BCryptPasswordEncoder的bean对象即可。加密方式会自动加盐;

            第三步:配置类中注入bean对象:

    1. /**
    2. * @author Alex
    3. */
    4. @Configuration
    5. public class SecurityConfig{
    6. @Bean
    7. public PasswordEncoder passwordEncoder(){
    8. return new BCryptPasswordEncoder();
    9. }
    10. }

        将密码字符串加密,调用encode()将密码加密。将加密后的字符串存入数据库;

    1. @Test
    2. public void testPasswordEncoder1(){
    3. String encode = securityConfig.passwordEncoder().encode("123");
    4. System.err.println(encode);
    5. }
    6. @Test
    7. public void testPasswordEncoder(){
    8. BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    9. String encode = passwordEncoder.encode("123");
    10. System.err.println(encode);
    11. }

             当注入bean对象后,明文前加{noop}就不可用了。

         简单提一下解密:

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

    3.3、自定义登录/认证:

            第一步:自定义登录页面

            第二步:定义一个登录接口

    1. /**
    2. * 登录方法、登录成功跳转到首先、
    3. * 否则继续跳转登录页,并给出提示
    4. * @param username
    5. * @return
    6. */
    7. @PostMapping("/login")
    8. public Map userLogin(String username, String password){
    9. TbUser loginUser = new TbUser();
    10. loginUser.setUserName(username);
    11. loginUser.setPassWord(password);
    12. return userService.userLogin(loginUser);
    13. }

            第三步:放行登录接口、请求登录接口

    1. @Bean
    2. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    3. http
    4. //关闭csrf
    5. .csrf().disable()
    6. .authorizeRequests()
    7. // 允许匿名访问的接口
    8. .antMatchers("/user/login").anonymous()
    9. .antMatchers("/toLogin").anonymous()
    10. // 访问toUserAdd接口需要admin角色
    11. .antMatchers("/toUserAdd").hasAnyRole("admin")
    12. // 访问toUserEdit接口需要edit权限
    13. // .antMatchers("/toUserEdit").hasAnyAuthority("edit")
    14. // .antMatchers("/toUserEdit").hasAuthority("edit")
    15. .antMatchers("/toUserList").hasAnyAuthority("list")
    16. // 除上面外的所有请求全部需要鉴权认证
    17. .anyRequest().authenticated();
    18. http.formLogin()
    19. // 访问登录页面接口
    20. .loginPage("/toLogin")
    21. // 执行登录方法接口
    22. .loginProcessingUrl("user/login");
    23. // http.logout().logoutUrl("/logout");
    24. return http.build();
    25. }

            第四步:在Service层使用ProviderManager的authenticate()方法进行验证

    将封装的Authentication对象 存入SecurityContextHolder

    1. @Override
    2. public Map userLogin(TbUser loginUser, HttpSession session) {
    3. Map responseMap = new HashMap<>(2);
    4. try {
    5. UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassWord(),null);
    6. Authentication authenticate = authenticationManager.authenticate(token);
    7. // 存入SecurityContextHolder
    8. SecurityContextHolder.getContext().setAuthentication(authenticate);
    9. responseMap.put("code","0");
    10. return responseMap;
    11. }catch (RuntimeException e){
    12. responseMap.put("code","-1");
    13. e.printStackTrace();
    14. return responseMap;
    15. }
    16. }

    此处直接返回map,也可以封装一个返回结果集对象,然后对于Security的 Session Management相关的内容会在后续文章中更新。

     前端监听表单提交后发送登录请求:

    1. //登录请求
    2. const url = "user/login";
    3. $.post(url,data,function(response){
    4. console.log(response.code);
    5. if(response.code==0){
    6. layer.msg("登录成功",{icon:6,time:1000}, function () {
    7. window.location = '/';
    8. });
    9. }else {
    10. layer.msg("用户名或密码错误",{icon:5,anim:6});
    11. $("#btn-login").removeAttr("disabled", "disabled").removeClass("layui-btn-disabled");
    12. }
    13. });

    到这里呢、自定义登录就完成了、看下登录后的跳转的首页

            实现效果:

            3.4、后端接口拦截

                    1、在SecurityFilterChain中完成配置

                    2、在接口上加注解

                    若需接口注解生效,配置类上加@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')")

            3.5、前端按钮隐藏

     此时前端的页面时包括所有的的接口按钮的,需要对将不属于当前角色或者没有权限的接口异常,为了标签生效,后端需要引入:thymeleaf整合security的yongriyong'ri

    1. <dependency>
    2. <groupId>org.thymeleaf.extrasgroupId>
    3. <artifactId>thymeleaf-extras-springsecurity5artifactId>
    4. dependency>

    配置类中加入:

    1. /**
    2. * 页面的sec:标签生效
    3. * @return
    4. */
    5. @Bean
    6. public SpringSecurityDialect springSecurityDialect(){
    7. return new SpringSecurityDialect();
    8. }

    前端需要引入security的命名空间:

    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"

    index.html改造后: 所提供的标签属性和配置类的用法一致;

    1. html>
    2. <html lang="en" xmlns:th="http://www.thymeleaf.org"
    3. xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
    4. >
    5. <head>
    6. <meta charset="UTF-8">
    7. <title>首页title>
    8. head>
    9. <body>
    10. <h1>hello,SpringSecurity、我是<span sec:authentication="principal.username">span>h1>
    11. <div sec:authorize="!isAuthenticated()">
    12. <a class="item" th:href="@{/toLogin}">登录a>
    13. div>
    14. <div sec:authorize="isAuthenticated()">
    15. <h2>角色及权限:<span sec:authentication="principal.authorities">span>h2>
    16. div>
    17. <div sec:authorize="isAuthenticated()">
    18. <a class="item" th:href="@{/logout}">注销a>
    19. div>
    20. <br>
    21. <hr>
    22. <br>
    23. <h1>基于角色展示:h1>
    24. <div>
    25. <div sec:authorize="hasRole('admin')">
    26. <a th:href="@{/toUserAdd}"> 添加用户a>
    27. <a th:href="@{/toUserDel}"> 删除用户a>
    28. <a th:href="@{/toUserEdit}"> 修改用户a>
    29. div>
    30. <div sec:authorize="hasAnyRole('user')">
    31. <a th:href="@{/toUserList}"> 用户列表a>
    32. div>
    33. div>
    34. <br>
    35. <hr>
    36. <br>
    37. <h1>基于权限展示:h1>
    38. <div>
    39. <div sec:authorize="hasAnyAuthority('add')">
    40. <a th:href="@{/toUserAdd}"> 添加用户a>
    41. div>
    42. <div sec:authorize="hasAuthority('delete')">
    43. <a th:href="@{/toUserDel}"> 删除用户a>
    44. div>
    45. <div sec:authorize="hasAnyAuthority('edit','select')">
    46. <a th:href="@{/toUserEdit}"> 修改用户a>
    47. div>
    48. <div sec:authorize="hasAuthority('select')">
    49. <a th:href="@{/toUserList}"> 用户列表a>
    50. div>
    51. div>
    52. body>
    53. html>

    实现效果:

     

                    3.6、自定义注册

    提交表单:拿到密码后对传入的密码进行随机盐+哈希散列加密、然后将随机盐和加密后的字符串存入数据库用户表中、并初始化一些用户及权限就🆗了。

                  3.7、关于拓展

    将用户信息存入SecurityContextHolder的上下文只在springboot单体项目中可以使用。

    前后端分离的情况下可以将token封装为JWT存入缓存。从缓存中取到并解析字符串也可获取用户信息。

    过程中的一些报错

    1、Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.

    解决:配置文件加上

    debug: true

    2、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......")

    1. @SpringBootApplication
    2. @MapperScan("com.example.demo.mapper")
    3. public class DemoApplication {
    4. public static void main(String[] args) {
    5. SpringApplication.run(DemoApplication.class, args);
    6. }
    7. }

    认证过程:

    1、springSecurity如何校验用户名、密码和权限?

    答:通过一个登录请求的debug得到如下流程
            发送一个登录请求----->UsernamePasswordAuthenticationFilter--->通过authenticate()方法认证----->loadUserUsername()方法获得UserDetails对象-->从该对象中拿到密码对比系统中的密码---->给UserDetails对象添加权限并设置到Authentication中,存入SecurityContentHolder中即可。  (完成一个cookie--session的闭环)

    2、 为什么从数据库中查到角色code需要加上前缀 "ROLE_"再封装到CustomerUserDetails对象中?

            个人理解是为了区分权限和角色的编码。我们点进关于登录的主体是否包括某个角色的验证源码时,在对应的验证方法中可以找到答案。也可以自定义验证方式。

    private String defaultRolePrefix = "ROLE_";
    1. public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {
    2. String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
    3. if (grantedAuthorityDefaultsBeanNames.length == 1) {
    4. GrantedAuthorityDefaults grantedAuthorityDefaults = (GrantedAuthorityDefaults)context.getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class);
    5. this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
    6. } else {
    7. this.rolePrefix = "ROLE_";
    8. }
    9. this.REGISTRY = new ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry(context);
    10. }

    传送门:

    初始化SQL脚本

    springboot整合thymeleaf

    springboot整合mybatis

    springboot整合mybatis-plus

    springboot整合shiro实现简单权限控制

  • 相关阅读:
    利用cpolar为群晖NAS建立稳定外网地址(1)
    基于Arrow的轻量线程池
    电脑windows,ubuntu系统vnc-viewer和服务器ubuntu的连接,以及灰屏现象处理
    (02)Cartographer源码无死角解析-(20) MapBuilder→MapBuilder()构造函数
    高级性能测试系列《20. 事务控制器、在性能测试中,看聚合报告的前提条件是?》...
    智能合约语言(eDSL)—— 如何使用wasmtime运行合约
    基于JSP的图书销售管理系统
    MySQL 中 count() 和 count(1) 有什么区别?哪个性能最好?
    Javascript如何获取到字符串的第一位元素
    10分钟读懂数据响应式和双向绑定原理
  • 原文地址:https://blog.csdn.net/chenyunjiangNN/article/details/127249244