0
点赞
收藏
分享

微信扫一扫

通俗易懂的SpringSecurity学习笔记

陈情雅雅 2022-05-03 阅读 51

文章目录

基本概念介绍

源码:https://gitee.com/DoubleW2w/spring-security-learning

介绍

SpringSecurity 安全服务框架,核心功能是认证和授权。提供了生命是安全访问控制功能,减少为了系统安全而编写大量重复代码的工作。

认证

认证即判断用户的身份是否合法,合法则继续访问,不合法则拒绝访问。

  • 用户密码登录,二维码登录,手机短信登录,人脸识别,指纹认证

目的:保护系统隐私数据与资源

授权

认证通过后,根据用户的权限来控制用户访问资源的过程。普通用户只能看普通资源,VIP用户能看VIP资源。

认证是保证用户身份的合法性,授权则是为了更细粒度对数据进行划分,控制不同的用户能够访问不同的资源

项目搭建

依赖pom文件

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- SpringMVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- MyBatisPlus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.4</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
package com.doublew2w.controller;

@Controller
public class PageController {

    @RequestMapping("/{page}")
    public String showPage(@PathVariable String page) {
        return page;
    }
}
package com.doublew2w;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

resources.templates

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登录首页</title>
</head>
<body>
    <h1>主页面</h1>
</body>
</html>

resources

server:
  port: 80
#日志格式
logging:
  pattern:
    console: '%d{yyyy-MM-dd HH:mm:ss, SSS} %clr(%-5level) --- [%thread]  %cyan(%-50logger{50}):%msg%n'

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mysecurity?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

启动项目,访问项目主页面 http://localhost/main,项目会自动跳转到一个登录页面。这代表Spring Security已经开启了认证
功能,不登录无法访问所有资源,该页面就是 Spring Security 自带的登录页面。

我们使用 user 作为用户名,控制台中的字符串作为密码登录,登录成功后跳转到项目主页面。

内存认证

Spring Security 会将登录页传来的用户名密码和内存中的用户名密码做匹配认证,使用到的类是 InMemoryUserDetailsManager

@Configuration
public class SecurityConfig {
    /**
     * 定义认证逻辑
     *
     * @return 认证逻辑Bean
     */
    @Bean
    public UserDetailsService userDetailsService() {
        // 1.使用内存数据进行认证
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 2.创建两个用户
        UserDetails user1 = User.withUsername("baizhan").password("123").authorities("admin").build();
        UserDetails user2 = User.withUsername("sxt").password("456").authorities("admin").build();
        // 3.将这两个用户添加到内存中
        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    /**
     * 密码编码器,不解析密码
     *
     * @return 密码解析器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

将登录页传来的用户名密码和内存中用户名密码做匹配认证。

自定义认证逻辑

在实际项目中,认证逻辑是需要自定义控制的。将 UserDetailsService 接口的实现类放入Spring容器即可自定义认证逻辑

InMemoryUserDetailsManager 就是 UserDetailsService 接口的一个实现类,它将登录页传来的用户名密码和内存中用户名密码做匹配认证。当然我们也可以自定义 UserDetailsService 接口的实现类

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 的实现类必须重写 loadUserByUsername() 方法,该方法定义了具体的认证逻辑。

参数 username 是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个 UserDetails 对象,该对象是 Spring Security 提供的用户对象,包含用户名、密码、权限。Spring Security 会根据UserDetails 对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传image-20220501234341342

数据库建表

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'baizhan', 'baizhan', '13812345678');
INSERT INTO `users` VALUES (2, 'sxt', 'sxt', '13812345678');
INSERT INTO `users` VALUES (3, 'demo', '$2a$10$tBjD3Sdp6RGZdTlJoHns..3Yodf.DzahrkVHVoQGKSHCTxavCelHO', NULL);

编写实体类

@Data
public class Users {
    private Integer id;
    private String username;
    private String password;
    private String phone;
}

编写dao接口

public interface UsersMapper extends BaseMapper<Users> {
}

在 SpringBoot 启动类中添加 @MapperScan 注解,扫描Mapper文件夹

@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
public class Application {
    public static void main(String[] args)
    {
    	SpringApplication.run(MysecurityApplication.class, args);
    }
}

创建 UserDetailsService 的实现类,编写自定义认证逻辑

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UsersMapper usersMapper;
    // 自定义认证逻辑
    @Override
    public UserDetails
    loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.构造查询条件
        QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
        // 2.查询用户
        Users users = usersMapper.selectOne(wrapper);
        // 3.封装为UserDetails对象
        UserDetails userDetails = User
            .withUsername(users.getUsername())
            .password(users.getPassword())
            .authorities("admin")
            .build();
        // 4.返回封装好的UserDetails对象
        return userDetails;
    }
}

密码解析器PasswordEncoder

在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密码。

Spring Security要求容器中必须有 PasswordEncoder 实例,之前使用的 NoOpPasswordEncoderPasswordEncoder 的实现类,意思是不解析密码,使用明文密码。

Spring Security官方推荐的密码解析器是 BCryptPasswordEncoder

@SpringBootTest
public class PasswordEncoderTest {
    @Test
    public void testBCryptPasswordEncoder(){
        //创建解析器
        PasswordEncoder encoder = new BCryptPasswordEncoder();
            //密码加密
        String password = encoder.encode("baizhan");
        System.out.println("加密后:"+password);
        //密码校验
        /**
        * 参数1:明文密码
        * 参数2:加密密码
        * 返回值:是否校验成功
        */
        boolean result = encoder.matches("baizhan","$2a$10$/MImcrpDO21HAP2amayhme8j2SM0YM50/WO8YBH.NC1hEGGSU9ByO");
        System.out.println(result);
    }
}

BCryptPasswordEncoder 的实例放入Spring容器即可,并且在用户注册完成后,将密码加密再保存到数据库

@Configuration
public class SecurityConfig {

    /**
     * 密码编码器,不解析密码
     *
     * @return 密码解析器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

自定义登录界面

Spring Security也支持用户自定义登录页面

编写登录页面

在Spring Security配置类自定义登录页面

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    //Spring Security配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义表单登录
        http.formLogin().loginPage("/login.html") 
        // 自定义登录页面
        	.usernameParameter("username")
        // 表单中的用户名项
        	.passwordParameter("password")
        // 表单中的密码项
        	.loginProcessingUrl("/login")
        // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
        	.successForwardUrl("/main")
        //登录成功后跳转的路径
        	.failureForwardUrl("/fail");
        //登录失败后跳转的路径
        
        // 需要认证的资源
        http.authorizeRequests()
            .antMatchers("/login.html").permitAll() 
        // 登录页不需要认证
        	.anyRequest().authenticated();
        //其余所有请求都需要认证
        //关闭csrf防护
        http.csrf().disable();
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 静态资源放行
        web.ignoring().antMatchers("/css/**");
    }
}

Spring Security 为了防止CSRF攻击,默认开启了 CSRF 防护,这限制了除了GET请求以外的大多数方法。我们要想正常使用Spring
Security需要突破CSRF防护

  1. 解决方法一:关闭CSRF防护:http.csrf().disable();
  2. 解决方法二:要求访问时携带参数名为 _csrf值 为令牌,令牌在服务端产生,如果携带的令牌和服务端的令牌匹配成功,则正常访问
<form class="form" action="/login"method="post">
	<!-- 在表单中添加令牌隐藏域 -->
    <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
    <input type="text" placeholder="用户名" name="username">
    <input type="password" placeholder="密码" name="password">
    <button type="submit">登录</button>
</form>

fail.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>失败页面</title>
</head>
<body>
<h1>登录失败,请<a href="/login.html">重新登录</a></h1>
</body>
</html>

login.html

<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录</title>
    <link href="/css/styles.css" rel="stylesheet" >
</head>
<body>
    <div class="htmleaf-container">
        <div class="wrapper">
            <div class="container">
                <h1>Welcome</h1>
                <form class="form" action="/login" method="post">
                    <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
                    <input type="text" placeholder="用户名" name="username">
                    <input type="password" placeholder="密码" name="password">
                    <input type="checkbox" name="remember-me" value="true"/>记住我</br>
                    <button type="submit" id="login-button">登录</button>
                </form>
            </div>
        </div>
    </div>
</body>
</html>

会话管理

需求场景:用户认证通过后,有时我们需要获取用户信息,比如在网站顶部显示:欢迎您,XXX。

Spring Security将用户信息保存在会话中,并提供会话管理,我们可以从 SecurityContext 对象中获取用户信息,SecurityContext 对象与当前线程进行绑定。

获取用户信息的写法

@RestController
public class MyController {

    // 获取当前登录用户名
    @RequestMapping("/users/username")
    public String getUsername(){
        // 1.获取会话对象
        SecurityContext context = SecurityContextHolder.getContext();
        // 2.获取认证对象
        Authentication authentication = context.getAuthentication();
        // 3.获取登录用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return userDetails.getUsername();
    }
}

当我们的登录成功后,去请求 /users/username 会返回登录用户的用户名

认证成功后的处理方式

登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。

自定义登录成功处理器

public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 拿到登录用户的信息
        UserDetails userDetails =(UserDetails)authentication.getPrincipal();
        System.out.println("用户名:"+userDetails.getUsername());
        System.out.println("一些操作...");
        // 重定向到主页
        response.sendRedirect("/main");
    }
}

配置登录成功处理器

http.formLogin() // 使用表单登录
    .loginPage("/login.html") // 自定义登录页面
    .usernameParameter("username") // 表单中的用户名项
    .passwordParameter("password") // 表单中的密码项
    .loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
    //.successForwardUrl("/main") //登录成功后跳转的路径
    .successHandler(new MyLoginSuccessHandler()) //登录成功处理器
    .failureForwardUrl("/fail"); //登录失败后跳转的路径

认证失败后的处理方式

登录失败后,如果除了跳转页面还需要执行一些自定义代码时,如:统计失败次数记录日志等,可以自定义登录失败处理器。

public class MyLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
        System.out.println("记录失败日志...");
        response.sendRedirect("/fail");
    }
}

配置登录失败处理器

http.formLogin() // 使用表单登录
	.loginPage("/login.html") // 自定义登录页面
    .usernameParameter("username") // 表单中的用户名项
    .passwordParameter("password") // 表单中的密码项
    .loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
	//
    .successForwardUrl("/main") //登录成功后跳转的路径
    .successHandler(new MyLoginSuccessHandler()) //登录成功处理器
	//
    .failureForwardUrl("/fail") //登录失败后跳转的路径
    .failureHandler(new MyLoginFailureHandler()); //登录失败处理器
	// 需要认证的资源
	http.authorizeRequests()
        .antMatchers("/login.html").permitAll() //登录页不需要认证
        .antMatchers("/fail").permitAll() //失败页不需要认证
        .anyRequest().authenticated(); //其余所有请求都需要认证

退出登录

退出登录后,Spring Security进行了以下操作:

  • 清除认证状态
  • 销毁HttpSession对象
  • 跳转到登录页面

main.html

添加退出登录连接

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="shortcuticon" href="resources/favicon.ico" th:href="@{/static/favicon.ico}">
    <title>登录首页</title>
</head>
<body>
<h1>主页面</h1>
<a href="/logout">退出登录</a>
</body>
</html>

配置退出登录

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义表单登录
        http.formLogin()
            // 自定义登录页面
            .loginPage("/login.html")
            // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
            .loginProcessingUrl("/login")
            // 表单中的用户名项
            .usernameParameter("username")
            // 表单中的密码项
            .passwordParameter("password")
            // 登录成功处理器
            .successHandler(new MyLoginSuccessHandler())
            // 登录失败处理器
            .failureHandler(new MyLoginFailureHandler());

        // 关闭csrf防护
        http.csrf().disable();

        // 退出登录配置
        http.logout()
            // 登出路径
            .logoutUrl("/logout")
            // 登出成功后请求的页面
            .logoutSuccessUrl("/login.html")
            //清除认证状 态,默认为true
            .clearAuthentication(true)
            // 销毁HttpSession对象,默认为true
            .invalidateHttpSession(true);
    }

	....
}

退出成功处理器

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("清除一些数据...");
        // 重定向到登录页面
        httpServletResponse.sendRedirect("/login.html");
    }
}

配置退出成功处理器

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义表单登录
        http.formLogin()
            // 自定义登录页面
            .loginPage("/login.html")
            // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
            .loginProcessingUrl("/login")
            // 表单中的用户名项
            .usernameParameter("username")
            // 表单中的密码项
            .passwordParameter("password")
            // 登录成功处理器
            .successHandler(new MyLoginSuccessHandler())
            // 登录失败处理器
            .failureHandler(new MyLoginFailureHandler());

        // 关闭csrf防护
        http.csrf().disable();

        // 退出登录配置
        http.logout()
            // 登出路径
            .logoutUrl("/logout")
			// 登出处理器
            .logoutSuccessHandler(new MyLogoutSuccessHandler())
            //清除认证状 态,默认为true
            .clearAuthentication(true)
            // 销毁HttpSession对象,默认为true
            .invalidateHttpSession(true);
    }
.....
}

记住我

当使用“记住我”功能登录后,Spring Security 会生成一个令牌,令牌一方面保存到数据库中,另一方面生成一个叫 remember-me 的Cookie保存到客户端。

image-20220502181457662

添加记住我配置类

@Configuration
public class RememberConfig {
    @Resource
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
        // Spring Security 自带的令牌控制器
        JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl = new JdbcTokenRepositoryImpl();
        jdbcTokenRepositoryImpl.setDataSource(dataSource);
        //自动建表,第一次启动时需要,第二次启动时注释掉
        //jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
        return jdbcTokenRepositoryImpl;
    }
}

image-20220502181607374

修改配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PersistentTokenRepository persistentTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义表单登录
        http.formLogin()
            // 自定义登录页面
            .loginPage("/login.html")
            // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
            .loginProcessingUrl("/login")
            // 表单中的用户名项
            .usernameParameter("username")
            // 表单中的密码项
            .passwordParameter("password")
            // 登录成功处理器
            .successHandler(new MyLoginSuccessHandler())
            // 登录失败处理器
            .failureHandler(new MyLoginFailureHandler());

        // 关闭csrf防护
        http.csrf().disable();

        // 退出登录配置
        http.logout()
            // 登出路径
            .logoutUrl("/logout")
            // 登出成功后请求的页面
            //.logoutSuccessUrl("/login.html")
            .logoutSuccessHandler(new MyLogoutSuccessHandler())
            //清除认证状 态,默认为true
            .clearAuthentication(true)
            // 销毁HttpSession对象,默认为true
            .invalidateHttpSession(true);

        // 记住我配置
        http.rememberMe()
            // 登录逻辑交给哪个对象
            .userDetailsService(userDetailsService)
            // 持久层对象
            .tokenRepository(persistentTokenRepository)
            // 保存时间(秒)
            .tokenValiditySeconds(30);
    }

	.....
}

在登录页面添加“记住我复选框”

<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录</title>
    <link href="/css/styles.css" rel="stylesheet" >
</head>
<body>
<div class="htmleaf-container">
    <div class="wrapper">
        <div class="container">
            <h1>Welcome</h1>
            <form class="form" action="/login" method="post">
                <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
                <input type="text" placeholder="用户名" name="username">
                <input type="password" placeholder="密码" name="password">
                <input type="checkbox" name="remember-me" value="true"/>记住我</br>
                <button type="submit" id="login-button">登录</button>
            </form>
        </div>
        <ul class="bg-bubbles">
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
        </ul>
    </div>
</div>
</body>
</html>

授权

授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。RBAC是业界普遍采用的授权方式

Role-Based Access Control

即按角色进行授权。比如在企业管理系统中,主体角色为总经理可以查询企业运营报表。

if(主体.hasRole("总经理角色")){
	查询运营报表
}

这样会造成,当查询运营报表的角色发生变化时,代码也要相关的进行改变

Resource-Based Access Control

基于资源的访问控制,即按资源(或权限)进行授权。比如在企业管理系统中,用户必须 具有查询报表权限才可以查询企业运营报表。

if(主体.hasPermission("查询报表权限")){
	查询运营报表
}

授权__权限表设计

用户、角色、权限都是多对多的关系

image-20220502182133261
这种方式需要指定用户有哪些权限,如:张三有查询工资的权限,即在用户权限中间表中添加一条数据,分别记录张三和查询工资权限ID。但在系统中权限数量可能非常庞大,如果一条一条添加维护数据较为繁琐。所以我们通常的做法是再加一张角色表:

image-20220502182225891

用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。

编写查询权限的方法

image-20220502185122342

image-20220503114950280

model包

public class BaseEntity {
    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.JSON_STYLE).toString();
    }
}

@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class Permission extends BaseEntity {
    /**
     * 权限id
     */
    private String pid;
    /**
     * 权限名称
     */
    private String permissionName;
    /**
     * 权限路径
     */
    private String url;
}

@Data
public class Role {
    /**
     * 角色id
     */
    private String rid;
    /**
     * 角色名称
     */
    private String roleName;
    /**
     * 角色描述
     */
    private String roleDesc;
}

@Data
public class Users {
    /**
     * 用户id
     */
    private Integer uid;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 手机号码
     */
    private String phone;
}

UsersMapper.java

public interface UsersMapper extends BaseMapper<Users> {
    /**
     * 根据用户名查询所有权限
     *
     * @param username 用户名
     * @return 结果
     */
    List<Permission> findPermissionAllByUsername(@Param("username") String username);
}
<?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.doublew2w.mapper.UsersMapper">
    <select id="findPermissionAllByUsername" resultType="com.doublew2w.model.Permission">
        SELECT DISTINCT e.pid,
                        e.permissionName,
                        e.url
        FROM users AS a
                 LEFT JOIN users_role AS b on a.uid = b.uid
                 LEFT JOIN role AS c on b.rid = c.rid
                 LEFT JOIN role_permission AS d on c.rid = d.rid
                 LEFT JOIN permission AS e on d.pid = e.pid
        where username = #{username}
    </select>
</mapper>

修改自认证逻辑

@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UsersMapper usersMapper;

    /**
     * 自定义认证逻辑
     *
     * @param username 用户名
     * @return 用户封装对象
     * @throws UsernameNotFoundException 异常
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.构造查询条件
        QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
        //2.查询结果
        Users users = usersMapper.selectOne(wrapper);
        if (users == null) {
            return null;
        }
        // 3.查询用户权限
        List<Permission> permissions = usersMapper.findPermissionAllByUsername(username);
        // 4.将自定义权限集合转为Security的权限类型集合 List<GrantedAuthority>
        List<GrantedAuthority> grantedAuthority = new ArrayList<>();
        for (Permission permission : permissions) {
            grantedAuthority.add(new SimpleGrantedAuthority(permission.getUrl()));
        }
        // 5.封装为UserDetails对象
        UserDetails userDetails = User.withUsername(users.getUsername())
                                      .password(users.getPassword())
                                      .authorities(grantedAuthority)
                                      .build();
        // 6.返回对象
        return userDetails;
    }
}

配置类设置访问控制

由于没有权限,访问被拦截,就会抛出403异常。

编写控制器类,添加控制器方法资源

@RestController
public class MyController {

    @GetMapping("/reportform/find")
    public String findReportForm() {
        return "查询报表";
    }

    @GetMapping("/salary/find")
    public String findSalary() {
        return "查询工资";
    }

    @GetMapping("/staff/find")
    public String findStaff() {
        return "查询员工";
    }
}

修改Security配置类

// 权限拦截配置
http.authorizeRequests()
    // 表示任何权限都可以访问
    .antMatchers("/login.html").permitAll()
    // 给资源配置需要的权限
    .antMatchers("/reportform/find").hasAnyAuthority("/reportform/find")
    .antMatchers("/salary/find").hasAnyAuthority("/salary/find")
    .antMatchers("/staff/find").hasAnyAuthority("/staff/find")
    //表示任何请求都需要认证后才能访问
    .anyRequest().authenticated();

自定义访问控制逻辑

我们可以自定义访问控制逻辑,即访问资源时判断用户是否具有名为该资源URL的权限

自定义访问控制逻辑

@Service
public class MyAuthorizationService {
    /**
     * 自定义访问控制逻辑,返回值为是否可以访问资源
     *
     * @param request        请求
     * @param authentication 权限
     * @return 结果
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        //获取会话中的登录用户
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            // 获取登录用户的权限
            Collection<? extends GrantedAuthority> authorities = ((UserDetails) principal).getAuthorities();
            // 获取请求的URL路径
            String uri = request.getRequestURI();
            // 将URL路径封装为权限对象
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(uri);
            // 判断用户的权限集合是否包含请求的URL权限对象
            return authorities.contains(authority);
        }
        return false;
    }
}

在配置文件中使用自定义访问控制逻辑

// 权限拦截配置
http.authorizeRequests()
    // 表示任何权限都可以访问
    .antMatchers("/login.html").permitAll()
    // 任何请求都使用自定义访问控制逻辑
  .anyRequest().access("@myAuthorizationService.hasPermission(request,authentication)");

注解设置访问控制

除了配置类,在SpringSecurity中提供了一些访问控制的注解。这些注解默认都是不可用的,需要开启后使用。
@Secured 注解是基于角色的权限控制,要求 UserDetails 中的权限名必须以
ROLE_ 开头。

启动类

@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

在控制器上添加注解

@Secured("ROLE_reportform")
@GetMapping("/reportform/find")
public String findReportForm() {
	return "查询报表";
}

不过该注解一般很少用,常用的是下面的注解 @PreAuthorize

// 权限拦截配置
http.authorizeRequests()
    // 表示任何权限都可以访问
    .antMatchers("/login.html").permitAll()
    .antMatchers("/fail").permitAll()
    .anyRequest().authenticated();

@PreAuthorize 注解可以在方法执行前判断用户是否具有权限

@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
@RestController
public class MyController {
    @PreAuthorize("hasAnyAuthority('/reportform/find')")
    @GetMapping("/reportform/find")
    public String findReportForm() {
        return "查询报表";
    }

    @PreAuthorize("hasAnyAuthority('/salary/find')")
    @GetMapping("/salary/find")
    public String findSalary() {
        return "查询工资";
    }

    @PreAuthorize("hasAnyAuthority('/staff/find')")
    @GetMapping("/staff/find")
    public String findStaff() {
        return "查询员工";
    }
}

在前端进行访问控制

当没有对应权限的时候,就没显示该菜单

引入依赖

<!--Spring Security整合Thymeleaf-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

修改main.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">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="shortcuticon" href="resources/favicon.ico" th:href="@{/static/favicon.ico}">
    <title>登录首页</title>
</head>
<body>
<h1>主页面</h1>
<ul>
    <li sec:authorize="hasAnyAuthority('/reportform/find')"><a href="/reportform/find">查询报表</a></li>
    <li sec:authorize="hasAnyAuthority('/salary/find')"><a href="/salary/find">查询工资</a></li>
    <!--    没有这个权限就不显示-->
    <li sec:authorize="hasAnyAuthority('/staff/find')"><a href="/staff/find">查询员工</a></li>
</ul>
<a href="/logout">退出登录</a>
</body>
</html>

编写当返回403的时候,处理页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>权限不足</title>
</head>
<body>
<h1>您的权限不足,请联系管理员!</h1>
</body>
</html>

编写权限不足处理类并配置

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.sendRedirect("/noPermission.html");
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PersistentTokenRepository persistentTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义表单登录
        http.formLogin()
            // 自定义登录页面
            .loginPage("/login.html")
            // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
            .loginProcessingUrl("/login")
            // 表单中的用户名项
            .usernameParameter("username")
            // 表单中的密码项
            .passwordParameter("password")
            // 登录成功处理器
            .successHandler(new MyLoginSuccessHandler())
            // 登录失败处理器
            .failureHandler(new MyLoginFailureHandler());

        // 关闭csrf防护
        http.csrf().disable();

        // 退出登录配置
        http.logout()
            // 登出路径
            .logoutUrl("/logout")
            // 登出成功后请求的页面
            //.logoutSuccessUrl("/login.html")
            .logoutSuccessHandler(new MyLogoutSuccessHandler())
            //清除认证状 态,默认为true
            .clearAuthentication(true)
            // 销毁HttpSession对象,默认为true
            .invalidateHttpSession(true);

        // 记住我配置
        http.rememberMe()
            // 登录逻辑交给哪个对象
            .userDetailsService(userDetailsService)
            // 持久层对象
            .tokenRepository(persistentTokenRepository)
            // 保存时间(秒)
            .tokenValiditySeconds(30);

        // 权限拦截配置
        http.authorizeRequests()
            // 表示任何权限都可以访问
            .antMatchers("/login.html").permitAll()
            .antMatchers("/fail").permitAll()
            .anyRequest().authenticated();

        //异常处理
        http.exceptionHandling()
            .accessDeniedHandler(new MyAccessDeniedHandler());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 静态资源放行
        web.ignoring().antMatchers("/css/**");
    }

    /**
     * 密码编码器,不解析密码
     *
     * @return 密码解析器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

总结

Spring Security

举报

相关推荐

0 条评论