Spring Security认证、加密、简单授权

淡淡的烟草味﹌ 2023-01-20 06:53 92阅读 0赞

目录

一、创建项目

二、内存认证

方式一:配置文件方式

方式二:配置类中重写configure方法

方式三:配置类中重写userDetailsService方法

方式四 :自定义实现类方式

三、数据库方式

方式1:基于默认数据库模型的认证

方式2:自定义数据库认证

四、加密

五、授权

角色继承


一、创建项目

使用Spingboot快速构建一个项目,引入以下依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. </dependency>

定义一个访问的Controller

  1. @Controller
  2. public class DemController {
  3. @RequestMapping("/index")
  4. public void index(){
  5. System.out.println("hello");
  6. }
  7. }

启动项目,访问/index,浏览器地址跳转为localhost:8080/login,界面如下

image.png

在项目启动过程中,我们会看到如下一行日志:

  1. Using generated security password: d58e2224-2034-4196-b13f-f4a18bb65a13

这就是 Spring Security 为默认用户 user 生成的临时密码,是一个 UUID 字符串。

在登录页面,默认的用户名就是 user,默认的登录密码则是项目启动时控制台打印出来的密码,输入用户名密码之后,就登录成功了,登录成功后,我们就可以访问到 /index接口了。

在 Spring Security 中,默认的登录页面和登录接口,都是 /login ,只不过一个是 get 请求(登录页面),另一个是 post 请求(登录接口)。

默认的密码有一个问题就是每次重启项目都会变。对于security登录认证账户可以来自内存和数据库,如下具体介绍。

二、内存认证

方式一:配置文件方式

默认的用户定义在SecurityProperties里边,是一个静态内部类,如果要定义自己的用户名密码,必然是要去覆盖默认配置,在配置文件中配置

  1. spring.security.user.name=admin
  2. spring.security.user.password=admin
  3. spring.security.user.roles=ADMIN

此时重启项目,就可以使用自己定义的用户名/密码登录了。

方式二:配置类中重写configure方法

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Bean
  4. PasswordEncoder passwordEncoder() {
  5. return NoOpPasswordEncoder.getInstance();
  6. }
  7. @Override
  8. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  9. auth.inMemoryAuthentication()
  10. .withUser("123")
  11. .password("123").roles("admin");
  12. }
  13. }
  • 首先我们自定义 SecurityConfig 继承自 WebSecurityConfigurerAdapter,重写里边的 configure 方法。
  • 首先我们提供了一个 PasswordEncoder 的实例,因为目前的案例还比较简单,因此我暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
  • configure 方法中,我们通过 inMemoryAuthentication 来开启在内存中定义用户,withUser 中是用户名,password 中则是用户密码,roles 中是用户角色。
  • 如果需要配置多个用户,用 and 相连

方式三:配置类中重写userDetailsService方法

由于 Spring Security 支持多种数据源,例如内存、数据库、LDAP 等,这些不同来源的数据被共同封装成了一个 UserDetailService 接口,任何实现了该接口的对象都可以作为认证数据源。因此我们还可以通过重写 WebSecurityConfigurerAdapter 中的 userDetailsService 方法来提供一个 UserDetailService 实例进而配置多个用户:

  1. @Configuration
  2. public class SecurityConfig1 extends WebSecurityConfigurerAdapter {
  3. @Bean
  4. PasswordEncoder passwordEncoder(){
  5. return NoOpPasswordEncoder.getInstance();
  6. }
  7. @Override
  8. @Bean
  9. public UserDetailsService userDetailsServiceBean() throws Exception {
  10. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
  11. manager.createUser(User.withUsername("admin").password("admin").roles("admin").build());
  12. manager.createUser(User.withUsername("123").password("123").roles("user").build());
  13. return manager;
  14. }
  15. }

方式四 :自定义实现类方式

重写UserDetailsService接口中的loadUserByUsername方法,并在配置类型申明

  1. import org.springframework.security.core.GrantedAuthority;
  2. import org.springframework.security.core.authority.AuthorityUtils;
  3. import org.springframework.security.core.userdetails.User;
  4. import org.springframework.security.core.userdetails.UserDetails;
  5. import org.springframework.security.core.userdetails.UserDetailsService;
  6. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  7. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  8. import org.springframework.stereotype.Service;
  9. import java.util.List;
  10. /**
  11. * 自定义认证服务实现security中的方法
  12. */
  13. @Service
  14. public class MyUserDetailsServiceImpl implements UserDetailsService {
  15. //实现security接口类userdetails唯一方法
  16. @Override
  17. public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
  18. //这个设置一个角色
  19. List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
  20. return new User("123",getPass(),authorities);
  21. }
  22. //对配置的用户密码123进行加密
  23. public String getPass(){
  24. return new BCryptPasswordEncoder().encode("123");
  25. }
  26. }
  27. import org.springframework.context.annotation.Bean;
  28. import org.springframework.context.annotation.Configuration;
  29. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  30. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  31. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  32. import org.springframework.security.crypto.password.PasswordEncoder;
  33. import javax.annotation.Resource;
  34. @Configuration
  35. public class SecurityConfig1 extends WebSecurityConfigurerAdapter {
  36. //注入自己的实现类
  37. @Resource
  38. private MyUserDetailsServiceImpl userDetailsService;
  39. @Bean
  40. PasswordEncoder passwordEncoder(){
  41. return new BCryptPasswordEncoder();
  42. }
  43. @Override
  44. public void configure(AuthenticationManagerBuilder auth) throws Exception {
  45. auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  46. }
  47. }

总结:以上四种基于内存的方式认证推荐第二种

三、数据库方式

方式1:基于默认数据库模型的认证

UserDetailsService 都有哪些实现类:

image

可以看到,在几个能直接使用的实现类中,除了 InMemoryUserDetailsManager 之外,还有一个 JdbcUserDetailsManager,使用 JdbcUserDetailsManager 可以让我们通过 JDBC 的方式将数据库和 Spring Security 连接起来。

JdbcUserDetailsManager 自己提供了一个数据库模型,这个数据库模型保存在如下位置:

  1. org/springframework/security/core/userdetails/jdbc/users.ddl

这里面sql语句如下

  1. create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
  2. create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
  3. create unique index ix_auth_username on authorities (username,authority);

可以看到,脚本中有一种数据类型 varchar_ignorecase,这个其实是针对 HSQLDB 数据库创建的,而我们使用的 MySQL 并不支持这种数据类型,所以这里需要大家手动调整一下数据类型,将 varchar_ignorecase 改为 varchar 即可。

修改完成后,创建数据库,执行完成后的脚本。

执行完 SQL 脚本后,我们可以看到一共创建了两张表:users 和 authorities。

  • users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
  • authorities 中保存了用户的角色。
  • authorities 和 users 通过 username 关联起来。

配置完成后,接下来,我们将 InMemoryUserDetailsManager 提供的用户数据用 JdbcUserDetailsManager 代替掉,如下:

  1. @Autowired
  2. DataSource dataSource;
  3. @Override
  4. @Bean
  5. protected UserDetailsService userDetailsService() {
  6. JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
  7. manager.setDataSource(dataSource);
  8. if (!manager.userExists("admin")) {
  9. manager.createUser(User.withUsername("admin").password("123").roles("admin").build());
  10. }
  11. if (!manager.userExists("123")) {
  12. manager.createUser(User.withUsername("123").password("123").roles("user").build());
  13. }
  14. return manager;
  15. }
  • 首先构建一个 JdbcUserDetailsManager 实例。
  • 给 JdbcUserDetailsManager 实例添加一个 DataSource 对象。
  • 调用 userExists 方法判断用户是否存在,如果不存在,就创建一个新的用户出来(因为每次项目启动时这段代码都会执行,所以加一个判断,避免重复创建用户)。
  • 用户的创建方法和我们之前 InMemoryUserDetailsManager 中的创建方法基本一致。

因为要连接数据库,我们还需导入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>mysql</groupId>
  7. <artifactId>mysql-connector-java</artifactId>
  8. </dependency>

配置文件配置

  1. spring.datasource.username=root
  2. spring.datasource.password=root
  3. spring.datasource.url=jdbc:mysql:///security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

方式2:自定义数据库认证

Spring Security适应系统,而非让系统适应Spring Security,是Spring Security框架开发者和使用者的共识,上面使用了 InMemoryUserDetailsManager 和 JdbcUserDetailsManager 两个UserDetailsService 实现类。生效方式也很简单,只需加入 Spring 的 IoC 容器,就会被 Spring Security自动发现并使用。自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService。

UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。UserDetails对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息

这里结合mybatis认证,需要导入的依赖

  1. <dependency>
  2. <groupId>org.mybatis.spring.boot</groupId>
  3. <artifactId>mybatis-spring-boot-starter</artifactId>
  4. <version>2.0.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. </dependency>

自定义类和配置

  1. @Service
  2. public class MyUserDetailsServiceImpl implements UserDetailsService {
  3. @Resource
  4. private UserDao userDao;
  5. //实现security接口类userdetails唯一方法
  6. @Override
  7. public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
  8. SysUser user = userDao.getUserByUserName(userName);
  9. //这里查出结果如:userName = "admin" passWord="$2a$10$FsNmqBMoxqRQAzcjvF8YD.Sqh3SaSkO40FfuC.VraGuKTcTeC3wDm";密码是经过BCryptPasswordEncoder加密的
  10. if(user==null){
  11. throw new UsernameNotFoundException("用户不存在");
  12. }
  13. //这个设置一个角色
  14. List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
  15. return new User(user.getUserName(),user.getPassWord(),authorities);
  16. }
  17. }
  18. @Configuration
  19. public class SecurityConfig1 extends WebSecurityConfigurerAdapter {
  20. //注入自己的实现类
  21. @Resource
  22. private MyUserDetailsServiceImpl userDetailsService;
  23. @Bean
  24. PasswordEncoder passwordEncoder(){
  25. return new BCryptPasswordEncoder();
  26. }
  27. @Override
  28. public void configure(AuthenticationManagerBuilder auth) throws Exception {
  29. auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  30. }
  31. }

附:

自定义相关配置

上面的登录地址都是security默认提供的,我们可以自定义一些配置

  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Bean
  4. PasswordEncoder passwordEncoder(){
  5. return new BCryptPasswordEncoder();
  6. }
  7. @Override
  8. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  9. auth.inMemoryAuthentication().withUser("admin").password("$2a$10$FsNmqBMoxqRQAzcjvF9YD.Sqh3SaSkO40FfuC.VraGuKTcTeC3wDm").roles("admin");
  10. }
  11. @Override
  12. public void configure(WebSecurity web) throws Exception {
  13. web.ignoring().antMatchers("/js/**", "/css/**","/img/**","/layui/**");
  14. }
  15. //自定义相关配置
  16. @Override
  17. protected void configure(HttpSecurity http) throws Exception {
  18. http.formLogin()//自定义自己编写的登录页面
  19. .loginPage("/login.html")//登录页面设置,默认去static下找页面
  20. .loginProcessingUrl("/adminLogin")//登录访问路径 这个controller不需要我们实现 security帮我们处理
  21. .defaultSuccessUrl("/admin/index")//登录成功之后跳转到自己的controller路径
  22. .and().logout().logoutUrl("/logout")//设置页面退出地址 这个controller不需要我们实现 security帮我们处理
  23. .logoutSuccessUrl("/admin/index")
  24. .and().authorizeRequests()
  25. .antMatchers("/","/adminLogin").permitAll()
  26. .antMatchers("/admin/**").authenticated()
  27. .anyRequest().permitAll()
  28. .and().csrf().disable();
  29. }
  30. }

form表单 需要放在boot项目下的static路径里

  1. <form action="/adminLogin" method="post" class="layui-form layui-form-pane" style="width: 24%;margin: auto;margin-top: auto;text-align: center;background-color: #f8f8f8;padding: 20px;padding: 49px 107px;">
  2. <img style="width: 100%;margin-bottom: 20px;" th:src="@{/static/img/logo.png}">
  3. <div class="layui-form-item">
  4. <label class="layui-form-label">账户</label>
  5. <div class="layui-input-block">
  6. <input type="text" name="username" placeholder="账户" class="layui-input" >
  7. </div>
  8. </div>
  9. <div class="layui-form-item">
  10. <label class="layui-form-label">密码</label>
  11. <div class="layui-input-block">
  12. <input type="password" name="password" placeholder="密码" class="layui-input">
  13. </div>
  14. </div>
  15. <button type="submit" class="layui-btn layui-btn-lg">登录</button>
  16. </form>
  • web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。
  • 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 <http>,HttpSecurity 提供的配置方法 都对应了该标签。
  • authorizeRequests 对应了 <intercept-url>
  • formLogin 对应了 <formlogin>
  • and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。
  • permitAll 表示登录相关的页面/接口不要被拦截。
  • 最后记得关闭 csrf
  • 当我们定义了登录页面为 /login.html 的时候,Spring Security 也会帮我们自动注册一个 /login.html 的接口,这个接口是 POST 请求,用来处理登录逻辑。

配置完成后,再去重启项目,此时访问任意页面,就会自动重定向到我们定义的这个页面上来,输入用户名密码就可以重新登录了。

四、加密

Spring Security 中加密是一个很简单却又不能忽略的模块,数据只有加密起来才更安全,这样就算据库密码泄漏也都是密文。本文分析对应的版本是 5.14。

Spring Security 为我们提供了一套加密规则和密码比对规则,org.springframework.security.crypto.password.PasswordEncoder 接口,该接口里面定义了三个方法。

  1. public interface PasswordEncoder {
  2. //加密(一般在注册的时候加密前端传过来的密码保存进数据库)
  3. String encode(CharSequence rawPassword);
  4. //加密前后对比(一般用来比对前端提交过来的密码和数据库存储密码, 也就是明文和密文的对比,我们只需提供对应对加密类 比对会自行处理)
  5. boolean matches(CharSequence rawPassword, String encodedPassword);
  6. //是否需要再次进行编码, 默认不需要
  7. default boolean upgradeEncoding(String encodedPassword) {
  8. return false;
  9. }
  10. }

该接口实现类有下面这几个

image.png

常用的

  • BCryptPasswordEncoder:Spring Security 推荐使用的,使用BCrypt强哈希方法来加密。
  • MessageDigestPasswordEncoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256…)
  • DelegatingPasswordEncoder:最常用的,根据加密类型id进行不同方式的加密,兼容性强
  • NoOpPasswordEncoder:明文, 不做加密
  • 其他

使用示例

MessageDigestPasswordEncoder

构造的时候需要传入算法字符串,例如 “MD5”、”SHA-1”、”SHA-256”…

  1. String password = "123";
  2. MessageDigestPasswordEncoder encoder = new MessageDigestPasswordEncoder("MD5");
  3. String encode = encoder.encode(password);
  4. System.out.println(encode);
  5. System.out.println(encoder.matches(password,encode) == true ? "相等" : "不相等");
  6. 输出
  7. {EUjIxnT/OVlk5J54s3LaJRuQgwTchm1gduFHTqI0qjo=}4b40375c57c285cc56c7048bb114db23
  8. 相等

调用 encode(..) 加密方法每次都会随机生成盐值,所以对相同的明文进行多次加密,每次结果也是不一样的。

从上面输出部分结合源码可以的出:加密的最终结果分为两部分,盐值 + MD5(password+盐值),调用 matches(..) 方法的时候先从密文中得到盐值,用该盐值加密明文和最终密文作对比。

BCryptPasswordEncoder

构造的时候可以传入哈希强度(strength),强度越大计算量就越大,也就意味着越安全,strength 取值区间[4-31],系统默认是10。

  1. String password = "123";
  2. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  3. String encode = encoder.encode(password);
  4. System.out.println(encode);
  5. System.out.println(encoder.matches(password, encode) == true ? "相等" : "不相等");
  6. 输出
  7. $2a$10$lxPfE.Zvat6tejB8Q1QGYu3M9lXUUpiWFYzboeyK64kbfgN9v7iBq
  8. 相等

调用 encode(..) 方法加密跟上面一样,每次都会随机生成盐值,密文也分为两部分,盐值和最终加密的结果,最终对比的时候从密文里面拿出盐值对明文进行加密,比较最终加密后的结果。

DelegatingPasswordEncoder

这是 Spring Security 推出的一套兼容方案,根据加密类型id字符串(idForEncode)去自身缓存的所有加密方式中(idToPasswordEncoder)取出对应的加密方案对象对明文进行加密和对应密文的对比,只是其密文前面都加上了加密方案id的字符串,具体的咱们看下面代码演示。

其初始化 Spring Security 提供了一个工厂构造方法

  1. public class PasswordEncoderFactories {
  2. @SuppressWarnings("deprecation")
  3. public static PasswordEncoder createDelegatingPasswordEncoder() {
  4. String encodingId = "bcrypt";
  5. Map<String, PasswordEncoder> encoders = new HashMap<>();
  6. encoders.put(encodingId, new BCryptPasswordEncoder());
  7. encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
  8. encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
  9. encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
  10. encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
  11. encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
  12. encoders.put("scrypt", new SCryptPasswordEncoder());
  13. encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
  14. encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
  15. encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
  16. return new DelegatingPasswordEncoder(encodingId, encoders);
  17. }
  18. }

这个工厂的静态构造方法把常用的几种方案都注入到缓存中,但是注入的 idForEncode 对应的却是 BCryptPasswordEncoder,这样系统就可以达到在新存储密码可以使用 BCryptPasswordEncoder 加密方案进行加密,但是对于数据库里面以前用其他方式加密的密码也支持比对。

  1. String password = "123";
  2. PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
  3. String encode = encoder.encode(password);
  4. System.out.println(encode);
  5. System.out.println(encoder.matches(password, encode) == true ? "相等" : "不相等");
  6. 输出
  7. {bcrypt}$2a$10$Bh23zGZ2YPOsORNexoowb.fX4QH18GEh13eVtZUZvbe2Blx0jIVna
  8. 相等

从结果中可以看出,相比原始的 BCryptPasswordEncoder 密文前面多了加密方式的id。

当然也可以自定义构造方法,来制定 DelegatingPasswordEncoder 用其他的方案进行加密。

接下来我们将其指定使用 MD5 方式来加密密码看看结果

  1. Map<String, PasswordEncoder> encoders = new HashMap<>();
  2. encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
  3. DelegatingPasswordEncoder encoder = new DelegatingPasswordEncoder("MD5", encoders);
  4. String encode = encoder.encode(password);
  5. System.out.println(encode);
  6. System.out.println(encoder.matches(password, encode) == true ? "相等" : "不相等");
  7. 输出
  8. {MD5}{XYwuzP8/lL/a3ASzA9UVM4rFs8lbsLvEoa5ydKER844=}d7f919bfd94554150f8ab3a809209ee3
  9. 相等

相比原始的 MessageDigestPasswordEncoder也是密文前面多了加密方式的id。

如果我们想同时使用多个密码加密方案,看来使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默认还不用配置。

首先我们来生成三个密码作为测试密码:

  1. @Test
  2. void contextLoads() {
  3. Map<String, PasswordEncoder> encoders = new HashMap<>();
  4. encoders.put("bcrypt", new BCryptPasswordEncoder());
  5. encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
  6. encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
  7. DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders);
  8. DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders);
  9. DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("noop", encoders);
  10. String e1 = encoder1.encode("123");
  11. String e2 = encoder2.encode("123");
  12. String e3 = encoder3.encode("123");
  13. System.out.println("e1 = " + e1);
  14. System.out.println("e2 = " + e2);
  15. System.out.println("e3 = " + e3);
  16. }

结果

  1. e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi
  2. e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2
  3. e3 = {noop}123

接下来,我们把这三个密码拷贝到 SecurityConfig 中去:

  1. @Configuration("aaa")
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. @Bean
  5. protected UserDetailsService userDetailsService() {
  6. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
  7. manager.createUser(User.withUsername("admin").password("{bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi").roles("admin").build());
  8. manager.createUser(User.withUsername("admintwo").password("{noop}123").roles("admin").build());
  9. manager.createUser(User.withUsername("dminthree").password("{MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2").roles("user").build());
  10. return manager;
  11. }
  12. @Override
  13. protected void configure(HttpSecurity http) throws Exception {
  14. http.authorizeRequests()
  15. .antMatchers("/admin/**").hasRole("admin")
  16. .antMatchers("/user/**").hasRole("user")
  17. ...
  18. }
  19. }

这里三个用户使用三种不同的密码加密方式。

配置完成后,重启项目,分别使用 admin/123、admintwo/123 以及 dminthree/123 进行登录,发现都能登录成功。

为什么我们会有这种需求?想在项目种同时存在多种密码加密方案?其实这个主要是针对老旧项目改造用的,密码加密方式一旦确定,基本上没法再改了(你总不能让用户重新注册一次吧),但是我们又想使用最新的框架来做密码加密,那么无疑,DelegatingPasswordEncoder 是最佳选择。

应用

对于我们开发者而言,我们通常都是在 SecurityConfig 中配置一个 PasswordEncoder 的实例,类似下面这样:

  1. @Bean
  2. PasswordEncoder passwordEncoder() {
  3. return new BCryptPasswordEncoder();
  4. }

剩下的事情,都是由系统调用的

  1. @Configuration
  2. @EnableWebSecurity
  3. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
  4. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  5. @Override
  6. public void configure(WebSecurity web) throws Exception {
  7. super.configure(web);
  8. }
  9. @Override
  10. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  11. auth.inMemoryAuthentication()
  12. .withUser("liuchao")
  13. .password("{bcrypt}$2a$10$S.hMD3oV60YRIj38lHRhP.e3DAu3OwmssE/u/p2GLqqZ3SVsZA77W")
  14. .roles("admin","user")
  15. .and()
  16. .passwordEncoder(passwordEncoder);
  17. }
  18. @Bean(value = "passwordEncoder")
  19. public PasswordEncoder delegatingPasswordEncoder() {
  20. //构造 DelegatingPasswordEncoder 加密方案
  21. return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  22. }
  23. @Autowired
  24. private PasswordEncoder passwordEncoder;
  25. }

五、授权

上面进行认证登录的同时指定了一些角色,以便授权,授权操作在Spring Security 的 configure(HttpSecurity http) 方法中进行,我们需要在配置类中重写如下两个方法,并进行相应的授权

  1. @Override
  2. public void configure(WebSecurity web) throws Exception {
  3. web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
  4. }
  5. @Override
  6. protected void configure(HttpSecurity http) throws Exception {
  7. http.authorizeRequests()
  8. .antMatchers("/admin/**").hasRole("admin")//只有admin角色才能访问admin地址
  9. .antMatchers("/user/**").hasRole("user")//只有user角色才能访问/user地址
  10. .anyRequest().authenticated();
  11. }

这里的匹配规则我们采用了 Ant 风格的路径匹配符,Ant 风格的路径匹配符在 Spring 家族中使用非常广泛,它的匹配规则也非常简单:






















通配符 含义
* 匹配多层路径
匹配一层路径
? 匹配任意单个字符

上面配置的含义是:

  1. 如果请求路径满足 /admin/** 格式,则用户需要具备 admin 角色。
  2. 如果请求路径满足 /user/** 格式,则用户需要具备 user 角色。
  3. 剩余的其他格式的请求路径,只需要认证(登录)后就可以访问。

注意代码中配置的三条规则的顺序非常重要,和 Shiro 类似,Spring Security 在匹配的时候也是按照从上往下的顺序来匹配,一旦匹配到了就不继续匹配了,所以拦截规则的顺序不能写错。另一方面,须将 anyRequest 配置在 antMatchers 前面

角色继承

要实现所有 user 能够访问的资源,admin 都能够访问,这涉及到另外一个知识点,叫做角色继承。

这在实际开发中非常有用。上级可能具备下级的所有权限,如果使用角色继承,这个功能就很好实现,我们只需要在 SecurityConfig 中添加如下代码来配置角色继承关系即可:

  1. @Bean
  2. RoleHierarchy roleHierarchy() {
  3. RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
  4. hierarchy.setHierarchy("ROLE_admin > ROLE_user");
  5. return hierarchy;
  6. }

注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。

发表评论

表情:
评论列表 (有 0 条评论,92人围观)

还没有评论,来说两句吧...

相关阅读