超级详细的SpringSecurity


概述

Spring 是⾮常流⾏和成功的 Java 应⽤开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基 于 Spring 框架,提供了⼀套 Web 应⽤安全性的完整解决⽅ 案。
正如你可能知道的关于安全⽅⾯的两个主要区域 是“认证”和“授权”(或者访问控 制),⼀般来说,Web 应⽤的安全性包括⽤户认证(Authentication)和⽤户授权 (Authorization)两个部分,这两点也是 Spring Security 重要核⼼功能。
(1)⽤户认证指的是:验证某个⽤户是否为系统中的合法主体,也就是说⽤户能否访问 该系统。⽤户认证⼀般要 求⽤户提供⽤户名和密码。系统通过校验⽤户名和密码来完成认 证过程。通俗点说就是系统认为⽤户是否能登录
(2)⽤户授权指的是验证某个⽤户是否有权限执⾏某个操作。在⼀个系统中,不同⽤户 所具有的权限是不同的。 ⽐如对⼀个⽂件来说,有的⽤户只能进⾏读取,⽽有的⽤户可以 进⾏修改。⼀般来说,系统会为不同的⽤户分配不 同的⻆⾊,⽽每个⻆⾊则对应⼀系列的 权限。通俗点讲就是系统判断⽤户是否有权限去做某些事情。

与shiro对比

Shiro和Spring Security⽐较 Shiro⽐Spring更容易使⽤、实现和理解。
Spring Security更加知名的唯⼀原因是因为品牌名称“Spring”以简单⽽闻名,但讽刺的是很多⼈发现使⽤Spring Security很困难。但是,Spring Security却有更好的社区⽀持。
Apache Shiro与Spring Security处理密码学⽅⾯相⽐有⼀个额外的模块。
Spring Security 与Spring 结合地较好,如果项⽬⽤的springmvc ,使⽤起来很⽅便。但是如果项⽬中没有⽤到 spring框架,那就不要考虑它了。
Shiro 功能强⼤、且简单、灵活。是Apache基⾦会下的项⽬,⽐较可靠,且不跟任何框架或者容器绑定,可以独⽴运⾏。
官网介绍

快速入门

  1. 创建spring脚手架项目(导入web)
  2. 添加依赖(在脚手架中添加serurity就不需要添加依赖)
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  1. 运行Application类
  2. 会出现一个需要用户名密码的页面,用户名为user,密码为控制台打印的密码,输入即可进入index页面

底层原理

官方文档底层原理
image.png
image.png

Filter

DelegatingFilterProxy

DelegatingFilterProxy 是 Spring Security 提供的⼀个 Filter 实现,可以在 Servlet 容器和 Spring 容器之间建⽴桥 梁。通过使⽤ DelegatingFilterProxy,这样就可以将Servlet容器中的 Filter 实例放在 Spring 容器中管理。

FilterChainProxy

复杂的业务中不可能只有⼀个过滤器。因此FilterChainProxy是Spring Security提供的⼀个特殊的Filter,它允许通 过SecurityFilterChain将过滤器的⼯作委托给多个Bean Filter实例。

SecurityFilterChain

SecurityFilterChain 被 FilterChainProxy 使⽤,负责查找当前的请求需要执⾏的Security Filter列表。

Multiple SecurityFilterChain

可以有多个SecurityFilterChain的配置,FilterChainProxy决定使⽤哪个SecurityFilterChain。如果请求的URL 是/api/messages/,它⾸先匹配SecurityFilterChain0的模式/api/**,因此只调⽤SecurityFilterChain 0。假设没有 其他SecurityFilterChain实例匹配,那么将调 SecurityFilterChain n。

自定义登录

流程解析

官方文档Java自定义配置
1. 登录
1.⾃定义登录接⼝
调⽤ProviderManager的⽅法进⾏认证如果认证通过⽣成jwt把⽤户信息存⼊redis中
2.⾃定义UserDetailsService
在这个实现列中去查询数据库

  1. 校验:

1.定义jwt认证过滤器
获取token
解析token获取其中的userid从redis中获取⽤户信息 存⼊SecurityContextHolde

基于内存的用户认证实现

自定义接口

package com.ry.securitydemo.config;
@Configuration
//@EnableWebSecurity//Spring项⽬中需要添加此注解,SpringBoot项⽬中不需要
public class WebSecurityConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser( //此⾏设置断点可以查看创建的user对象
            User
            .withDefaultPasswordEncoder()
            .username("admin") //⾃定义⽤户名
            .password("123456") //⾃定义密码
            .roles("USER") //⾃定义⻆⾊
            .build()
        );
        return manager;
    }
}

登录界面只需要输入自定义的用户名和密码即可

基于数据库的用户登录

实现步骤

  • 程序启动时:
    • 创建 DBUserDetailsManager 类,实现接⼝ UserDetailsManager, UserDetailsPasswordService
    • 在应⽤程序中初始化这个类的对象
  • 校验⽤户时:
    • SpringSecurity⾃动使⽤ DBUserDetailsManager 的 象 loadUserByUsername ⽅法从 数据库中获取User对
    • 在UsernamePasswordAuthenticationFilter过滤器中的 attemptAuthentication ⽅法中将⽤户输 ⼊的⽤户名密码和从数据库中获取到的⽤户信息进⾏⽐较,进⾏⽤户认证

实现代码:基于springboot+mp

  1. **定义 DBUserDetailsManager **
@Component
public class DBUserDetailsManager implements UserDetailsManager {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper qw = new QueryWrapper();
        qw.eq("username",username);
        User user = userMapper.selectOne(qw);
        return org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()
                .username(user.getUsername())
                .password(user.getPassword())
                .roles("USER")
                .build();

    }

    @Override
    public void createUser(UserDetails user) {

        PasswordEncoder encoder = new BCryptPasswordEncoder();

        userMapper.insert(new User(user.getUsername(),encoder.encode(user.getPassword())));
    }

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


}

  1. ** 初始化UserDetailsService **

将WebSecurityConfig中的userDetailsService⽅法注释掉

  1. ** SpringSecurity的默认配置 **

在WebSecurityConfig中写如下代码:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    //authorizeRequests():开启授权保护
    //anyRequest():对所有请求开启授权保护
    //authenticated():已认证请求会⾃动被授权
    http
    .authorizeRequests(authorize -> authorize.anyRequest().authenticated())
    .formLogin(withDefaults())//表单授权⽅式
    .httpBasic(withDefaults());//基本授权⽅式
}

实现用户新增功能

controller

@Controller
public class UserController {


    @Autowired
    private UserService userService;

    @PostMapping("/addUser")
    @ResponseBody
    public String add(@RequestBody User user){

        userService.addUser(user);
        return "新增成功";
    }
}

service

接口

public interface UserService extends IService<User> {

    void addUser(User user);
}

实现类

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private DBUserDetailsManager dbUserDetailsManager;

    @Override
    public void addUser(User user) {
        dbUserDetailsManager.createUser(org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()
                .username(user.getUsername())
                .password(user.getPassword())
                .authorities(AuthorityUtils.createAuthorityList("ADMIN"))
                .build());
    }
}

修改配置

DBUserDetailsManager中添加⽅法

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

关闭csrf攻击防御

默认情况下SpringSecurity开启了csrf攻击防御的功能,这要求请求参数中必须有⼀个隐藏的_csrf字段,如下:
在filterChain⽅法中添加如下代码,关闭csrf攻击防御

//关闭csrf攻击防御
http.csrf((csrf) -> {
 csrf.disable();
 });

换到写的代码中就是(在WebSecurityConfig中)
image.png

修改默认的密码编码器

实际项⽬中我们不会把密码明⽂存储在数据库中,默认使⽤的PasswordEncoder要求数据库中的密码格式:(id}password。
它会根据id去判断密码的加密⽅式。
但是我们⼀般不会采⽤这种⽅式。所以就需要替换PasswordEncoder
我们⼀般使⽤SpringSecurity为我们提供的BCryptPasswordEncoder 我们只需要使⽤把BCryptPasswordEncoder对象注⼊Spring容器中,SpringSecurity就会使⽤该PasswordEncoder 来进⾏密码校验我们可以定义⼀个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承 WebsecurityConfigurerAdapter。

@Bean
 public PasswordEncoder passwordEncoder() {
 return new BCryptPasswordEncoder(); // 或者其他的PasswordEncoder实现
  }

或者(在DBUserDetailsManager中的createUser方法中进行密码修改)

 @Override
    public void createUser(UserDetails user) {

        PasswordEncoder encoder = new BCryptPasswordEncoder();

        userMapper.insert(new User(user.getUsername(),encoder.encode(user.getPassword())));
    }

Swagger测试

依赖

<!--swagger测试-->
<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
  <version>4.0.0</version>
</dependency>

在application.yml配置

knife4j:
  enable: true

密码加密算法

参考文档官网

密码加密方式

明文密码

最初,密码以明⽂形式存储在数据库中。但是恶意⽤户可能会通过SQL注⼊等⼿段获取到明⽂密码,或者程序员将 数据库数据泄露的情况也可能发⽣。

Hash算法

Spring Security的 PasswordEncoder 接⼝⽤于对密码进⾏ 要⽤到 单向转换,从⽽将密码安全地存储。对密码单向转换需 哈希算法,例如MD5、SHA-256、SHA-512等,哈希算法是单向的, 只能加密,不能解密。
因此, 数据库中存储的是单向转换后的密码,Spring Security在进⾏⽤户身份验证时需要将⽤户输⼊的密码进⾏单向 转换,然后与数据库的密码进⾏⽐较。
因此,如果发⽣数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通过 暴⼒破解的⽅式猜测密码。

彩虹表

恶意⽤户创建称为 彩虹表 的查找表
彩虹表就是⼀个庞⼤的、针对各种可能的字⺟组合预先⽣成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密 码,需要的彩虹表就越⼤,主流的彩虹表都是100G以上,⽬前主要的算法有LM, NTLM, MD5, SHA1, MYSQLSHA1, HALFLMCHALL, NTLMCHALL, ORACLE-SYSTEM, MD5-HALF

加盐密码

为了减轻彩虹表的效果,开发⼈员开始使⽤加盐密码。不再只使⽤密码作为哈希函数的输⼊,⽽是为每个⽤户的密 码⽣成随机字节(称为盐)。盐和⽤户的密码将⼀起经过哈希函数运算,⽣成⼀个唯⼀的哈希。盐将以明⽂形式与 ⽤户的密码⼀起存储。然后,当⽤户尝试进⾏身份验证时,盐和⽤户输⼊的密码⼀起经过哈希函数运算,再与存储 的密码进⾏⽐较。唯⼀的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。

自适应单向函数

随着硬件的不断发展,加盐哈希也不再安全。原因是,计算机可以每秒执⾏数⼗亿次哈希计算。这意味着我们可以 轻松地破解每个密码。 现在,开发⼈员开始使⽤⾃适应单向函数来存储密码。使⽤⾃适应单向函数验证密码时, 量的CPU、内存或其他资源)。⾃适应单向函数允许配置⼀个 故意占⽤资源(故意使⽤⼤ “⼯作因⼦”,随着硬件的改进⽽增加。我们建议将“⼯作 因⼦”调整到系统中验证密码需要约⼀秒钟的时间。这种权衡是为了 让攻击者难以破解密码。 ⾃适应单向函数包括 bcrypt、PBKDF2、scrypt和argon2。

PasswordEncoder(单向)

BCryptPasswordEncoder

使⽤⼴泛⽀持的bcrypt算法来对密码进⾏哈希。为了增加对密码破解的抵抗⼒,bcrypt故意设计得较慢。和其他⾃ 适应单向函数⼀样,应该调整其参数,使其在您的系统上验证⼀个密码⼤约需要1秒的时间。 BCryptPasswordEncoder的默认实现使⽤强度10。建议您在⾃⼰的系统上调整和测试强度参数,以便验证密码时 ⼤约需要1秒的时间。

Argon2PasswordEncoder

使⽤Argon2算法对密码进⾏哈希处理。Argon2是密码哈希⽐赛的获胜者。为了防⽌在⾃定义硬件上进⾏密码破 解,Argon2是⼀种故意缓慢的算法,需要⼤量内存。与其他⾃适应单向函数⼀样,它应该在您的系统上调整为⼤ 约1秒来验证⼀个密码。当前的Argon2PasswordEncoder实现需要使⽤BouncyCastle库。

Pbkdf2PasswordEncoder

使⽤PBKDF2算法对密码进⾏哈希处理。为了防⽌密码破解,PBKDF2是⼀种故意缓慢的算法。与其他⾃适应单向 函数⼀样,它应该在您的系统上调整为⼤约1秒来验证⼀个密码。当需要FIPS认证时,这种算法是⼀个很好的选择。

SCryptPasswordEncoder

使⽤scrypt算法对密码进⾏哈希处理。为了防⽌在⾃定义硬件上进⾏密码破解,scrypt是⼀种故意缓慢的算法,需 要⼤量内存。与其他⾃适应单向函数⼀样,它应该在您的系统上调整为⼤约1秒来验证⼀个密码。

密码加密测试

在测试类中编写一个测试方法

@Test
void testPassword() {
    // ⼯作因⼦,默认值是10,最⼩值是4,最⼤值是31,值越⼤运算速度越慢
    PasswordEncoder encoder = new BCryptPasswordEncoder(4);
    //明⽂:"password"
    //密⽂:result,即使明⽂密码相同,每次⽣成的密⽂也不⼀致
    String result = encoder.encode("password");
    System.out.println(result);

    //密码校验
    Assert.isTrue(encoder.matches("password", result), "密码不⼀致");

}

DelegatingPasswordEncoder

  • 表中存储的密码形式
    • {bcrypt} $2a 10 10 10GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW
  • 通过源码可以知道:可以通过 {bcrypt} 前缀动态获取和密码的形式类型⼀致的PasswordEncoder对象
  • 目的:方便随时做密码策略的升级,兼容数据库中的⽼版本密码策略⽣成的密码

自定义登录界面

使用Thymeleaf(不懂的话可以看springboot整合web开发)

创建登录Controller

@Controller
public class LoginController {
    @PostMapping("/login")
    public String login() {
        return "login";
    }
}

创建登录界面

resources/templates/login.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
  <head>
    <title>登录</title>
  </head>
  <body>
    <h1>登录</h1>
    <div th:if="${param.error}">
      错误的⽤户名和密码.</div>
    <!--method必须为"post"-->
    <!--th:action="@{/login}" ,
    使⽤动态参数,表单中会⾃动⽣成_csrf隐藏字段,⽤于防⽌csrf攻击
    login: 和登录⻚⾯保持⼀致即可,SpringSecurity⾃动进⾏登录认证-->
    <form th:action="@{/login}" method="post">
      <div>
        <!--name必须为"username"-->
        <input type="text" name="username" placeholder="⽤户名"/>
      </div>
      <div>
        <!--name必须为"password"-->
        <input type="password" name="password" placeholder="密码"/>
      </div>
      <input type="submit" value="登录" />
    </form>
  </body>
</html>

配置SecurityFilterChain

.formLogin( form -> {
    form
        .loginPage("/login").permitAll() //登录⻚⾯⽆需授权即可访问
        .usernameParameter("username") //⾃定义表单⽤户名参数,默认是username
        .passwordParameter("password") //⾃定义表单密码参数,默认是password
        .failureUrl("/login?error") //登录失败的返回地址
        ;
 }); //使⽤表单授权⽅式

image.png
登录成功进入index界面

用户认证

认证成功

 .successHandler(new MyAuthenticationSuccessHandler())//用户登录成功之后执行的内容、

image.png

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //转发
        String username = request.getParameter("username");
        request.setAttribute("username",username);
        request.getRequestDispatcher("/success").forward(request,response);
    }
}

认证失败

.failureHandler(new MyAuthenticationFailureHandler())//用户失败后干嘛

image.png

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        //返回json数据给前端
        response.setContentType("application/json;charset=utf-8");
        String s = JSON.toJSONString("登录失败");
        response.getWriter().write(s);
    }
}

用户授权

授权管理的实现在SpringSecurity中⾮常灵活,可以帮助应⽤程序实现以下两种常⻅的授权需求:

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

用户-权限-资源

在WebSecurityConfig中

//开启授权保护,这是springsecurity6的授权⽅式
http.authorizeRequests(
    authorize -> authorize
    //具有USER_LIST权限的⽤户可以访问/user/list
    .requestMatchers("/user/list").hasAuthority("USER_LIST")
    //具有USER_ADD权限的⽤户可以访问/user/add
    .requestMatchers("/user/add").hasAuthority("USER_ADD")
    //对所有请求开启授权保护
    .anyRequest()
    //已认证的请求会被⾃动授权
    .authenticated()
);
//springsecurity5授权⽅式
//      
http.authorizeRequests(authorize ->
                       授予权限 
                       DBUserDetailsManager中的loadUserByUsername⽅法:
                       请求未授权的接⼝ 
                       ⽤户授权

                       //                authorize.antMatchers("/user/list").hasAuthority("ADMIN")
                       //                        .antMatchers("/user/add").hasAuthority("USER")
                       //                        .anyRequest()
                       //                        .authenticated()
                       //        );
                       //springsecurity5官⽹授权⽅式
                       http.authorizeRequests(authorize ->
                                              authorize.mvcMatchers("/user/list").hasAuthority("ADMIN")
                                              .mvcMatchers("/user/add").hasAuthority("USER")
                                              .anyRequest()
                                              .authenticated()
                                             );

对应的在DBUserDetailsManager中loadUserByUsername方法返回值要写.authorities(AuthorityUtils.createAuthorityList(“ADMIN”))

用户-角色-资源

在WebSecurityConfig中

 http
                .authorizeHttpRequests(authorize ->
                        authorize
                                .mvcMatchers("/addUser").hasAnyRole("ADMIN","MEMBER")
                                .mvcMatchers("/delUser").hasAnyRole("ADMIN","MEMBER")
                                .mvcMatchers("/updUser").hasRole("ADMIN")
                                .mvcMatchers("/selUser").hasAnyRole("ADMIN","VISITOR")
                                .anyRequest()//对所有请求开启授权保护
                                .authenticated()//已认证的请求会被自动授权
                );

对应的在DBUserDetailsManager中loadUserByUsername方法返回值要写.roles(“ADMIN”)

注解方式

开启方法授权

在WebSecurityConfig加入下面注解

@EnableMethodSecurity(prePostEnabled = true)

给用户授予角色和权限

DBUserDetailsManager中的loadUserByUsername⽅法 :

return org.springframework.security.core.userdetails.User
  .withUsername(user.getUsername())
  .password(user.getPassword())
  .roles("ADMIN")
  .authorities("USER_ADD", "USER_UPDATE")
  .build();

常用授权注解

//⽤户必须有 ADMIN ⻆⾊ 并且 ⽤户名是 admin 才能访问此⽅法
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'admim'")
 @GetMapping("/list")
 public List<User> getList(){
    return userService.list();
 }

 //⽤户必须有 USER_ADD 权限 才能访问此⽅法
@PreAuthorize("hasAuthority('USER_ADD')")
 @PostMapping("/add")
 public void add(@RequestBody User user){
    userService.saveUserDetails(user);
}

用户授权失败

在WebSecurityConfig

 http
                .exceptionHandling()
                .accessDeniedHandler(new MyAccessDeniedHandler());//用户登录成功,认证失败
 public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, 
AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //获取异常信息
        String message = accessDeniedException.getMessage();
        //创建map集合,将提示信息存储到map集合中去
        Map map=new HashMap();
        map.put("code",403);
        map.put("msg","您当前未被授权,不能访问");
        //将数据转储成json字符串格式
        String jsonString = JSON.toJSONString(map);
        //将json字符串写回给客户端
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(jsonString);
    }
 }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@Zeal

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值