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的整合依赖,其他根据需要导入

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

        <!--数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--        MP 起步依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.0</version>
            <!--   <scope>test</scope> -->
        </dependency>

        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--thymeleaf模块引擎-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--   test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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、密码在控制台。

        

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

3.1、Security的过滤链:

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

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

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

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

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

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

spring:
  security:
    user:
      name: zs
      password: 123

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

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

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

        

@Data
@NoArgsConstructor
public class CustomerUserDetails implements UserDetails {



    TbUser user;
    List<String> permissions;



    public CustomerUserDetails(TbUser user,List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    /**
     * 权限集合:包括了角色code、可访问权限code
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = 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;
    }

}

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

        实现loadUserByUsername方法。

    @Override
    public UserDetails loadUserByUsername(String username){
        //1、根据用户名查询用户信息
        LambdaQueryWrapper<TbUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TbUser::getUserName,username);
        TbUser user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        // 2、查询角色及权限:
        List<String> authoritiesList = new ArrayList<>();
        List<String> authoritiesPerms = userMapper.selectPermission(user);
        List<TbRole>  roleList = userMapper.selectRole(user);
        // 若 数据库代表角色的code没有加 ROLE_ 作为前缀、需要手动加一下;
        List<String> authoritiesRoles = this.addPrefix(roleList);

        authoritiesList.addAll(authoritiesPerms);
        authoritiesList.addAll(authoritiesRoles);
        return new CustomerUserDetails(user,authoritiesList);
    }

    /**
     * 给role_code 加上前缀
     * @param roleList
     * @return
     */
    private List<String> addPrefix(List<TbRole> 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对象即可。加密方式会自动加盐;

        第三步:配置类中注入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函数得到的字符串到已加密的数据库中进行匹配。

3.3、自定义登录/认证:

        第一步:自定义登录页面

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

    /**
     * 登录方法、登录成功跳转到首先、
     * 否则继续跳转登录页,并给出提示
     * @param username
     * @return
     */
    @PostMapping("/login")
    public Map<String, String> 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();
    }

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

将封装的Authentication对象 存入SecurityContextHolder

    @Override
    public Map<String, String> userLogin(TbUser loginUser, HttpSession session) {
        Map<String, String> responseMap = 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");
                }
            });

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

        实现效果:

        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

        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

配置类中加入:

    /**
     * 页面的sec:标签生效
     * @return
     */
    @Bean
    public SpringSecurityDialect springSecurityDialect(){
        return new SpringSecurityDialect();
    }

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

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

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

<!DOCTYPE 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 th:text="${session.userName}"></span></h1><br>-->
<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>

实现效果:

 

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

@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);
    }

传送门:

初始化SQL脚本

springboot整合thymeleaf

springboot整合mybatis

springboot整合mybatis-plus

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

  • 10
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Spring BootSpring Security是一对好朋友,Spring Boot提供了强大的自动配置和快速开发的能力,而Spring Security则提供了完整的安全解决方案,可以实现用户认证、授权、安全过滤等功能。本文将介绍如何在Spring Boot整合Spring Security实现权限控制。 1. 添加Spring Security依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 2. 配置Spring SecuritySpring Boot中,可以通过application.properties或application.yml文件配置Spring Security。以下是一个简单的配置: ``` spring.security.user.name=admin spring.security.user.password=123456 spring.security.user.roles=ADMIN ``` 这个配置定义了一个用户名为admin,密码为123456,角色为ADMIN的用户。在实际应用中,应该将用户名和密码存储在数据库或其他安全存储中。 3. 创建SecurityConfig类 创建一个继承自WebSecurityConfigurerAdapter的SecurityConfig类,并重写configure方法: ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin").password("{noop}123456").roles("ADMIN"); } } ``` configure方法定义了应用程序的安全策略,这里配置了所有请求都需要认证(即登录)才能访问,除了首页和登录页,这两个页面可以匿名访问。formLogin方法配置了自定义的登录页面,logout方法配置了退出登录的操作。 configureGlobal方法定义了一个内存中的用户,用户名为admin,密码为123456,角色为ADMIN。在实际应用中,应该将用户信息存储在数据库或其他安全存储中。 4. 创建登录页面 在templates目录下创建一个名为login.html的登录页面,例如: ``` <!DOCTYPE html> <html> <head> <title>Login Page</title> </head> <body> <h1>Login Page</h1> <div th:if="${param.error}"> Invalid username and password. </div> <div th:if="${param.logout}"> You have been logged out. </div> <form th:action="@{/login}" method="post"> <div> <label>Username:</label> <input type="text" name="username" /> </div> <div> <label>Password:</label> <input type="password" name="password" /> </div> <div> <button type="submit">Login</button> </div> </form> </body> </html> ``` 5. 运行应用程序 在浏览器中访问http://localhost:8080/login,输入用户名admin和密码123456,即可登录成功。如果输入错误的用户名或密码,则会提示“Invalid username and password.”。如果成功登录后再访问http://localhost:8080/home,则可以看到“Welcome home!”的欢迎消息。 6. 实现权限控制 上面的例子中只实现了登录认证,没有实现权限控制。下面介绍如何实现权限控制。 首先需要在configureGlobal方法中添加更多的用户和角色: ``` @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin").password("{noop}123456").roles("ADMIN") .and() .withUser("user").password("{noop}password").roles("USER"); } ``` 这里定义了一个管理员用户和一个普通用户,分别拥有ADMIN和USER两个角色。 然后在configure方法中添加更多的安全策略: ``` @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } ``` 这里添加了一个安全策略,即/admin/**路径需要拥有ADMIN角色才能访问。 现在管理员用户可以访问/admin/**路径,而普通用户则不能访问。如果普通用户尝试访问/admin/**路径,则会提示“Access is denied”。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值