Spring Security

Spring Security

它是Spring家族中的一个安全管理框架,具备功能有:身份认证、授权、防御常见攻击(CSRF、HTTP Headers、HTTP Requests),它的底层原理是传统的Servlet过滤器

官方文档:

https://docs.spring.io/spring-security/reference/index.html

spring-security官方案例:

https://github.com/spring-projects/spring-security-samples/tree/main)

自定义配置

基于内存的用户认证

UserDetailsService用来管理用户信息

@Configuration
@EnableWebSecurity //Spring项目总需要添加此注解
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser( 
            User
            .withDefaultPasswordEncoder()
            .username("yan")     //自定义用户名
            .password("123456")  //自定义密码
            .roles("USER")       //自定义角色
            .build()
        );
        return manager;
    }
}

再次登录页面,必须输入所设置的用户名和密码,才可以登录成功,否则失败,不再使用Security默认的用户名和密码。

基于数据库的数据源

基础sql脚本

-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;

-- 创建用户表
CREATE TABLE `user`(
	`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
	`username` VARCHAR(50) DEFAULT NULL COMMENT '用户名',
	`password` VARCHAR(500) DEFAULT NULL COMMENT '密码',
	`enabled` BOOLEAN NOT NULL COMMENT '是否启动'
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`); 

-- 插入用户(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);

完成基础的增删改查…

@Configuration
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Resource
    private UserMapper userMapper;  // 数据库 - 用户表mapper
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // sql - 查询用户表 (根据用户名,进行查询用户表)
        User user = userMapper.selectOne(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    user.getEnabled(),
                    true,        // 用户账号是否过期
                    true,        // 用户凭证是否过期
                    true,        // 用户是否未被锁定
                    authorities);// 权限列表
        }
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }

    // 创建用户
    @Override
    public void createUser(UserDetails userDetails) {
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        user.setEnabled(true);
        userMapper.insert(user);
    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }

}

加密算法

Hash算法:

Spring Security的PasswordEncoder接口用于对密码进行单向转换,使用哈希算法,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密。缺点:通过破解的方式猜测密码。

彩虹表:

恶意用户创建称为彩虹表的查找表,越是复杂的密码,需要的彩虹表就越大。

加盐密码:

盐是随机生产的字节(是明文的),盐和用户的密码一起经过哈希函数运算,生产一个哈希值,再与库的密码进行比较,可避免彩虹表,每个盐和密码它的哈希值都是不同的。

自适应单向函数:

随着硬件的不断发展,加盐哈希也不再安全。因为计算机可以每秒执行亿级别哈希运行,所有自适应单向函数,产生一个工作因子,来故意占用CPU、内存,让攻击者很难破解。

PasswordEncoder

  • BCryptPasswordEncoder:使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。
  • Argon2PasswordEncoder:使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。
  • Pbkdf2PasswordEncoder:使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。
  • SCryptPasswordEncoder:使用scrypt算法对密码进行哈希处理。为了防止在自定义硬件上进行密码破解,scrypt是一种故意缓慢的算法

自定义登录页

创建对应的html的登录页,在Security自定义配置类里,添加:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests(a -> {
            a.anyRequest()      // 对所有请求开启授权保护
             .authenticated();  // 已认证的请求会自动授权
        })
        .formLogin(f -> {
            f.loginPage("/login").permitAll() // 登录页面无需授权即可访问
             .usernameParameter("username")   // 自定义表单用户名参数,默认是username
             .passwordParameter("password")   // 自定义表单密码参数,默认是password
             .failureUrl("/login?error");     //登录失败的返回地址
        }); // 表单授权方式
    http.csrf(csrf -> csrf.disable());  // 暂时关闭csrf攻击
    return http.build();
}

前后端分离

用户认证

  • 登录成功后调用:AuthenticationSuccessHandler
  • 登录失败后调用:AuthenticationFailureHandler

需要重写这2个过滤器,然后在Security自定义配置类进行对应的配置。

注销处理

  • 注销处理调用:LogoutSuccessHandler

需要重写这个过滤器,然后在Security自定义配置类进行对应的配置。

请求未认证的接口

当访问没有授权认证的接口,默认的Spring Security会使用AuthenticationEntryPoint将用户请求跳转到登录页面,要求用户提供登录凭证,如果自定义的,可以设置返回的信息进行处理。

  • 未认证处理调用:AuthenticationEntryPoint

需要重写这个过滤器,然后在Security自定义配置类进行对应的配置。

跨域

# 在Security自定义配置类 这里添加
http.cors(withDefaults());

获取用户信息

登录页面成功,在其他方法中可以得到当前登录用户的信息

// 存储认证对象的上下文
SecurityContext context = SecurityContextHolder.getContext();
// 认证对象
Authentication authentication = context.getAuthentication();
// 用户名
String username = authentication.getName();
// 身份
Object principal = authentication.getPrincipal();
// 凭证(脱敏)
Object credentials = authentication.getCredentials();
//权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

会话并发

当我们同一个用户登录人数过多,会存在账号风险,可通过实现接口SessionInformationExpiredStrategy处理,然后在Security自定义配置类进行对应的配置。

//会话管理
http.sessionManagement(session -> {
    session
        .maximumSessions(1) // 代表当前登录同一个用户,数量超过1,触发该逻辑
        .expiredSessionStrategy(new MySessionInformationExpiredStrategy()); // 重写的SessionInformationExpiredStrategy接口,处理返回逻辑
});

授权

  • 用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表
  • 用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息

用户权限资源的配置:

// 1.配置权限 [自定义配置]
http.authorizeRequests(
    authorize -> authorize
    //具有LIST权限的用户可以访问/user/list
    .requestMatchers("/user/list").hasAuthority("LIST")
    //具有ADD权限的用户可以访问/user/add
    .requestMatchers("/user/save").hasAuthority("ADD")
    //对所有请求开启授权保护
    .anyRequest()
    //已认证的请求会被自动授权
    .authenticated()
);

// 2 在登录页的接口,为当前登录的用户,赋值给对应的权限列表。
 Collection<GrantedAuthority> authorities = new ArrayList<>();
 authorities.add(() -> "LIST");
 authorities.add(() -> "ADD");
  • 未授权处理JSON信息调用:AccessDeniedHandler

需要重写这个过滤器,然后在Security自定义配置类进行对应的配置。

用户角色资源配置:

// 1.配置权限 [自定义配置]
http.authorizeRequests(
    authorize -> authorize
    // 具有管理员角色(ADMIN)的用户可以访问/user/**
    .requestMatchers("/user/**").hasRole("ADMIN")
    //对所有请求开启授权保护
    .anyRequest()
    //已认证的请求会被自动授权
    .authenticated()
);

// 2 在登录页的接口,为当前登录的用户,赋值给对应的权限列表。
UserDetails user = new org.springframework.security.core.userdetails.User
                    .withUsername(user.getUsername())
                    .password(user.getPassword())
                    .disabled(!user.getEnabled())  // 是否被禁用
                    .credentialsExpired(false)     // 是否过期, false:未过期
                    .accountLocked(false)          // 是否锁定, false:没有锁定
                    .roles("ADMIN")               // 角色 - 有权限 - 正常访问接口
//                    .roles("ADMIN222")           // 角色  - 没有授权的角色就是403
                    .build();

用户角色权限资源配置

这样我们需要四张表(用户表、角色表、权限表、用户角色关联表),用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限,通过对用户角色关联和角色权限关联表进行操作,可以管理访问权限。

# 用户表:
user_id   int      用户ID  
username  varchar  用户名  
password  varchar  密码

# 角色表:
role_id   int      角色ID
name      varchar  角色名称
des       varchar  描述

# 权限表:
permission_id int  权限ID
name      varchar  权限名称
des       varchar  描述

# 角色权限关联表
role_permission_id int  角色权限关联ID
role_id   int      角色ID
permission_id int  权限ID

基于方法的授权

在配置文件中添加如下注解

@EnableMethodSecurity

在用户登录授权,进行授权角色或者权限资源

// 一般我们的角色、权限资源是通过SQL数据库中获取的
UserDetails user = org.springframework.security.core.userdetails.User
        .withUsername(user.getUsername())
        .password(user.getPassword())
        .disabled(!user.getEnabled())  // 是否被禁用
        .credentialsExpired(false)     // 是否过期, false:未过期
        .accountLocked(false)          // 是否锁定, false:没有锁定
        .roles("ADMIN")                // 角色配置
        .authorities("USER_ADD", "USER_UPDATE")  // 权限资源,一但配置,上面的 roles 角色,会失效
        .build();

常用授权注解

//用户必须有 ADMIN 角色 并且 用户名是 admin 才能访问此方法
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'admim'")
@GetMapping("/list")
public String getList(){
    return "list接口,返回信息”;
}

//用户必须有 USER_ADD 权限 才能访问此方法
@PreAuthorize("hasAuthority('USER_ADD')")
@PostMapping("/add")
public void add(@RequestBody User user){
    return "add接口,返回信息”;
}

OAuth2简介

OAuth2是一种开放授权协议。

OAuth 2协议包含以下角色:

  1. 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。
  2. 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。
  3. 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。
  4. 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。

四种授权模式:

  • 授权码(authorization-code)

    这种是最常用,最复杂,也是最安全的,客户端向服务端获取Token,服务端返回授权码,客户端拿到授权码再向服务端请求Token,得到Token。

  • 隐藏式(implicit)

    这种客户端向服务端获取Token,服务端返回对应的Token,使用场景安全不高的,令牌的有效期非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

  • 密码式(password)

    这种是客户端向服务端获取Token,输入正确的密码,就可以得到Token,使用信任度很高的服务,比如:你登录A服务,输入B服务的用户名和密码,也可以登录成功A服务。

  • 客户端凭证(client credentials)

    这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌,只要有凭证就可以得到Token。

一般我们访问令牌是面向机器使用,会选择客户端凭证模式;如果是web应用、浏览器访问应用使用授权码模式;如果是企业内部系统,可以使用密码式模式;如果原生APP、自己开发单页面,可以使用隐藏式模式;如果对第三方应用的页面,使用授权码模式。

GiuHub社交登录案例

登录GitHub,登录成功,点击个人头像,找到Settings菜单,然后在左侧菜单,找到Developer Settings,选择OAuth Apps,为客户应用创建访问GitHub的凭据。

在这里插入图片描述

创建完,该位置可以查看,该项目有哪些用户已经授权了,已经授权的用户,下次访问可以直接访问用户数据。

在这里插入图片描述

默认的重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId}。registrationId是ClientRegistration的唯一标识符。

获取应用程序id (Client ID),生成应用程序密钥。要记住,后续在application.yml配置文件进行配置

Ov23lifcZ4GRQu3esNda

6beb497f0c5128d676b8936b45a73619c75f26a5

创建Spring项目

  1. 引包
<!-- 客户应用 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 在该版本,thymeleaf 需要该包进行辅助 -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
  1. 进行配置文件
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            # 上面的client id
            client-id: xxx
            client-secret: xxxx
#            redirectUri: http://localhost:8200/login/oauth2/code/github
  1. 静态页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <title>Spring Security - OAuth 2.0 Login</title>
    <meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
    <div style="float:left">
        <span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
    </div>
    <div style="float:none">&nbsp;</div>
    <div style="float:right">
        <form action="#" th:action="@{/logout}" method="post">
            <input type="submit" value="Logout" />
        </form>
    </div>
</div>
<h1>OAuth 2.0 Login with Spring Security</h1>
<div>
    You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>
    via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>
</div>
<div>&nbsp;</div>
<div>
    <span style="font-weight:bold">User Attributes:</span>
    <ul>
        <li th:each="userAttribute : ${userAttributes}">
            <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
        </li>
    </ul>
</div>
</body>
</html>
  1. 请求层代码
/**
 * 首页
 * 1. 输入:http://localhost:8080/ 地址
 * 会跳转到 github 的地址,进行登录,输入 github 的账号和密码,登录成功后,进行授权
 * 我们访问到该页面:http://localhost:8080/?continue  ,该页面,有很多github的该用户的信息
 * 2。 退出 在右侧有logout ,退出的按钮,点击进行退出,http://localhost:8080/login?logout
 * 3. 再次点击 github,页面,任然可以有很多github的该用户的信息
 */
@Controller
public class IndexController {

    /**
     * 登录成功后】,跳转该地址
     *
     * @param model
     * @param authorizedClient
     * @param oauth2User       记录当前用户登录的信息
     * @return {@link String}
     */
    @GetMapping("/")
    public String index(
            Model model,
            @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
            @AuthenticationPrincipal OAuth2User oauth2User) {
        model.addAttribute("userName", oauth2User.getName());
        model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
        model.addAttribute("userAttributes", oauth2User.getAttributes());
        return "index";
    }

}

这样我们,登录该项目,会跳转到github的登录页面,进行授权,如果授权成功,会得到该用户的github的用户信息。

登录流程

  1. A 网站让用户跳转到 GitHub,并携带参数ClientID 以及 Redirection URI。
  2. GitHub 要求用户登录,然后询问用户"A 网站要求获取用户信息的权限,你是否同意?"
  3. 用户同意,GitHub 就会重定向回 A 网站,同时发回一个授权码。
  4. A 网站使用授权码,向 GitHub 请求令牌。
  5. GitHub 返回令牌.
  6. A 网站使用令牌,向 GitHub 请求用户数据。
  7. GitHub返回用户数据
  8. A 网站使用 GitHub用户数据登录

CommonOAuth2Provider

它是一个预定义的通用OAuth2Provider,为一些知名资源服务API提供商;

例如,授权URI、令牌URI和用户信息URI通常不经常变化。因此,提供默认值以减少所需的配置。

因此,当我们配置GitHub客户端时,只需要提供client-id和client-secret属性。

GITHUB {
    public ClientRegistration.Builder getBuilder(String registrationId) {
        ClientRegistration.Builder builder = this.getBuilder(
        registrationId, 
        ClientAuthenticationMethod.CLIENT_SECRET_BASIC, 
        
        //授权回调地址(GitHub向客户应用发送回调请求,并携带授权码)   
		"{baseUrl}/{action}/oauth2/code/{registrationId}");
        builder.scope(new String[]{"read:user"});
        //授权页面
        builder.authorizationUri("https://github.com/login/oauth/authorize");
        //客户应用使用授权码,向 GitHub 请求令牌
        builder.tokenUri("https://github.com/login/oauth/access_token");
        //客户应用使用令牌向GitHub请求用户数据
        builder.userInfoUri("https://api.github.com/user");
        //username属性显示GitHub中获取的哪个属性的信息
        builder.userNameAttributeName("id");
        //登录页面超链接的文本
        builder.clientName("GitHub");
        return builder;
    }
},
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值