草庐IT

【Spring Security】| 从0到1编写一个权限认证 | 学会了吗?

狮子也疯狂 2023-04-25 原文

目录

一. 🦁 认证前的工作

本次操作是基于SpringBoot项目的,使用Mybatis-Plus作为ORM框架,具体创建流程不再一一阐述。

1. 添加依赖

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

2. 创建数据库表(数据自行添加)

CREATE TABLE `users` (
  `uid` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `phone` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  PRIMARY KEY (`uid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

3. 编写用户实体类

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

4. 编写Dao接口

public interface UsersMapper extends BaseMapper<Users> {
}

5. 在启动类中添加 @MapperScan 注解

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.lion.mysecuritydemo.mapper")
public class MysecuritydemoApplication {

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

}

6. 继续添加各种包

二. 🦁 自定义逻辑认证原理—UserDetailsService

在项目中,认证逻辑一般是通过自定义实现的,将实现了UserDetailsService 接口的实现类放入Spring容器中,即可实现自定义逻辑认证。

实现UserDetailsService接口必须重写 loadUserByUsername方法,该方法定义了具体的认证逻辑,参数 username 是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个UserDetails对象,该对象是Spring Security提供的用户对象,包含用户名、密码、权限。Spring Security会根据UserDetails对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败,详细流程如下图:

三. 🦁 数据库认证

数据库认证是最常用的,我们现在来看看数据库认证应该怎么写?

其实就是按我们上面说的自定义一个MyUserDetailsService类,并且实现UserDetailsService接口,将其放入Spring容器中,如下:

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UsersMapper usersDao;

    /**
     * 自定义认证逻辑(现在是数据库认证)
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        1. 查询用户
        QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
        Users users = usersDao.selectOne(wrapper);

        if (users == null){
            return  null;
        }
//        2. 封装成UserDetails对象
        UserDetails userDetails = User.withUsername(users.getUsername())
                .password(users.getPassword())
                .authorities("admin")               //授权操作
                .build();

        return userDetails;
    }
}

四. 🦁 密文加密操作

在实际开发中,为了数据安全性,在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密

码。此时必须使用密码解析器才能将加密密码与明文密码做比对。Spring Security中的密码解析器是 PasswordEncoder 。

Spring Security要求容器中必须有 PasswordEncoder 实例,Spring Security官方推荐的密码解析器是 BCryptPasswordEncoder

我们在security配置类中加入如下一个方法即可:

  @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();

    }

五. 🦁自定义表单登录

虽然Spring Security给我们提供了登录页面,但在实际项目中,更多的是使用自己的登录页面。Spring Security也支持用户自定义登

录页面。用法如下:

1. 编写自定义页面

<!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="text" placeholder="用户名" name="username">
                <input type="password" placeholder="密码" name="password">
                <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>

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

在Spring Security配置类里继承WebSecurityConfigurerAdapter类,重写protected void configure(HttpSecurity http) 方法,如下:

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

这里使用的认证成功和失败跳转的处理方式是编写自定义成功和失败处理器(个人认为这个方法比较常用),因为登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。

3. 配置登录成功跳转处理器

public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//        1. 拿到登录用户信息
        UserDetails principal = (UserDetails) authentication.getPrincipal();


//      做一些需要的事情




//        2. 重定向回到主页
        response.sendRedirect("/main");


    }
}

4. 配置登录失败跳转处理器

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

5. 编写退出登录跳转处理器

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("清除一些数据...");
        response.sendRedirect("/login.html");
    }
}

6. 编写退出登录跳转配置

在系统中一般都有退出登录的操作。退出登录后,Spring Security进行了以下操作:

  • 清除认证状态

  • 销毁HttpSession对象

  • 跳转到登录页面

在Spring Security中,退出登录的写法如下:在security配置类里编写:

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

六. 🦁 关闭csrf防护

CSRF:

跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。 Spring Security

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

我们这里直接关闭csrf防护即可,在security配置类添加如下代码:

http.csrf().disable();

到这里认证工作就全部完成啦,现在来完成授权工作的编写!!!

——————over————————

七. 🦁 授权_RBAC

授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。

Resource-Based Access Control

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

表。逻辑为:

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

这样在系统设计时就已经定义好查询报表的权限标识,即使查询报表所需要的角色变化为总经理和股东也不需要修改授权代码,系统

可扩展性强。该授权方式更加常用。

八. 🦁 权限表设计

用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多

个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。

如:

张三拥有总经理的角色,而总经理拥有查询工资、查询报表的权限,这样张三就拥有了查询工资、查询报表的权限。这样管理用户时只需管理少量角色,而管理角色时也只需要管理少量权限即可。

我们在原有的Users表上,再添加角色表和权限表:

// 角色
@Data
public class Role {
    private String rid;
    private String roleName;
    private String roleDesc;
}
// 权限
@Data
public class Permission {
    private String pid;
    private String permissionName;
    private String url;
}

并且在UsersDao接口添加findPermissionByUsername方法。

// 根据用户名查询权限
List<Permission> findPermissionByUsername(String username);

这个方法设计五表查询,需要自定义编写sql语句:

SELECT DISTINCT
		permission.pid,permission.permissionName,permission.url
	FROM
		users
		LEFT JOIN users_role ON users_role.uid = users.uid
		LEFT JOIN role ON role.rid = users_role.rid
		LEFT JOIN role_permission ON role_permission.rid = role.rid
		LEFT JOIN permission ON role_permission.pid = permission.pid
	WHERE
		username = #{username}

九. 🦁 修改认证逻辑,认证成功后给用户授权

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UsersMapper usersDao;

    /**
     * 自定义认证逻辑(现在是数据库认证)
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        1. 查询用户
        QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
        Users users = usersDao.selectOne(wrapper);

        if (users == null){
            return  null;
        }
//        2. 查询用户权限
        List<Permission> permissions = usersDao.findPermissionByUsername(username);
        // 将自定义权限集合转为Security的权限类型集合
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        for (Permission permission : permissions) {
            grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));
        }
//        3. 封装成UserDetails对象
        UserDetails userDetails = User.withUsername(users.getUsername())
                .password(users.getPassword())
                .authorities(grantedAuthorities)               //授权操作
                .build();

        return userDetails;
    }
}

十. 🦁 设置访问控制的三种方式

在给用户授权后,我们就可以给系统中的资源设置访问控制,即拥有什么权限才能访问什么资源。

1. 准备工作

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

@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 "查询员工";
   }
}

2. 配置类设置访问控制

修改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();  				//表示任何请求都需要认证后才能访问

3. 自定义访问控制逻辑

如果资源数量很多,一条条配置需要的权限效率较低。我们可以自定义访问控制逻辑,即访问资源时判断用户是否具有名为该资源

URL的权限。

@Service
public class MyAuthorizationService {
    // 自定义访问控制逻辑,返回值为是否可以访问资源

    public boolean hasPermission(HttpServletRequest request, Authentication authentication){
//        获取认证的用户
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
//        获取登录用户的权限
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//        获取请求的URL路径
        String uri = request.getRequestURI();
//        将路径封装为权限对象
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(uri);
        return  authorities.contains(grantedAuthority);
    }

}

4. 注解设置访问控制

除了配置类,在SpringSecurity中提供了一些访问控制的注解。这些注解默认都是不可用的,需要开启后使用。

有两个,一个**@Secured**,因为使用麻烦,这里不细说。

我们来说说**@PreAuthorize**。该注解可以在方法执行前判断用户是否具有权限

① 在启动类开启注解使用

在启动类上方添加:

@EnableGlobalMethodSecurity(prePostEnabled = true)

② 在控制器方法上添加注解

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

到这里,一个权限表就完成啦!!!

有关【Spring Security】| 从0到1编写一个权限认证 | 学会了吗?的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  5. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  6. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  7. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  8. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  9. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  10. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

随机推荐