Spring Security框架

Spring Security框架

1.1. Spring Security简介

Spring Security是一个安全框架,在常规使用时,主要使用它的:

  • 框架内自带对密码进行加密的工具包,可以对密码进行加密处理;
  • 自带登录验证及获取登录用户的信息的机制(包括页面、提交数据后的处理),可以更加简单的实现登录、获取权限;
  • 可以便利的实现授权访问(即访问某个路径需要某个权限等);
  • 结合其它框架实现更多功能。

1.2. 添加依赖

在使用时,还是先在straw父级项目的pom.xml中添加依赖:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.3.2.RELEASE</version>
</dependency>

然后,在straw-api项目中添加依赖:

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

1.3. 使用Bcrypt算法处理加密

一般来说,在使用Spring Security时,推荐使用Bcrypt算法对密码进行加密处理!

关于Bcypt算法的使用:

@Test
void bcryptTests() {
    String password = "1234";
    String encodePassword = new BCryptPasswordEncoder().encode(password);
    System.out.println("[bcrypt] encode password=" + encodePassword);
}

如果多次运行,会发现每次运算结果都是不相同的(原文不变,密文一直都不同):
1.[bcrypt] encode password=$2a 10 10 10VUojUDNHcHGRsa/VF8sLyuDEKRPnMOW29hmDud0TjrDK7YwpWE9H6
2.[bcrypt] encode password=$2a 10 10 10wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO
3.[bcrypt] encode password=$2a 10 10 10twSBjL8sgGooL.a0kHjXjuQ9WcamDxsXN2fHR0B0tuIIfXY350I4G

即使对于同一个原文可以运算出N个不同的密文,在使用时,将任何一个密文存储到数据库中,然后将正确的原文提交给Spring Security后,Spring Security就可以完成“登录成功”的验证,当然,如果交给Spring Security的原文是错误的,则会“登录失败”,整个验证过程交给Spring Security框架去完成即可。

1.4. Spring Security登录验证

当Spring Boot项目集成了Spring Security的依赖后,默认情况下,所有的访问都是需要登录的!例如访问此前已经完成的注册功能时 http://localhost:8080/api/v1/users/student/register?phone=13100131001&password=1234&inviteCode=JSD1912-876840,会自动重定向到 http://localhost:8080/login:
在这里插入图片描述
以上登录页面的URL及页面都是由Spring Security框架内置的!

在使用Spring Security时,默认的用户名是user,默认的密码是启动项目时日志中提示的密码:
在这里插入图片描述
在Spring Security提供的登录页面中,会验证用户名与密码是否正确,如果登录失败,会提示:
在这里插入图片描述
如果登录成功,会自动重定向到登录之前尝试访问的页面,例如:在未登录时,直接访问“用户注册”,会因为未登录被重定向到“登录页面”,当登录成功后,会自动重定向到此前尝试访问的“用户注册”页面!

1.5. 在配置文件中配置Spring Security的登录账号

可以在application.properties中添加配置,定义登录Spring Security的用户名和密码:

# Spring Security临时使用的用户名和密码
spring.security.user.name=user
spring.security.user.password=1234

当添加以上配置后,再次重启项目,可以看到启动时不再提供临时密码,并且,登录时,输入以上配置的用户名和密码即可!

1.6. 通过内存配置Spring Security的登录账号

如果需要通过程序代码来指定登录的用户名和密码,需要自定义类,该类需要继承自WebSecurityConfigurerAdapter类:

package cn.tedu.straw.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * <p>创建“密码加密器”对象交给Spring框架进行管理!</p>
     * <p>后续,Spring Security将吃通过自动装配的机制得到这个密码加密器!</p>
     *
     * @return 密码加密器对象
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中授权
        auth.inMemoryAuthentication()
                // withUser > 指定用户名
                .withUser("root")
                // password > 指定密码
                .password("1234")
                // 指定授权
                .authorities("/admin/list");
    }

}

则重新启动项目,就可以通过root / 1234来登录了!

1.7. 关于授权-1

关于以上“授权”,配置的值是“权限标识”,是一个自定义的字符串(通常会写成URL路径的格式,但是,也可以随意写成其它格式),表示该用户具有哪些权限,如果需要验证权限,需要在配置类之前添加@EnableGlobalMethodSecurity注解,并显式的配置注解属性prePostEnabled的值为true(该属性默认值为false):

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * <p>创建“密码加密器”对象交给Spring框架进行管理!</p>
     * <p>后续,Spring Security将吃通过自动装配的机制得到这个密码加密器!</p>
     *
     * @return 密码加密器对象
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中授权
        auth.inMemoryAuthentication()
                // withUser > 指定用户名
                .withUser("root")
                // password > 指定密码
                .password("1234")
                // authorities > 指定授权,方法参数值是权限标识字符串
                .authorities("/admin/list");
    }

}

然后,可以写一个测试类,测试权限限制:

@RestController
public class TestController {

    @PreAuthorize("hasAuthority('/admin/list')")
    @GetMapping("/admin/list")
    public String adminList() {
        return "admin list";
    }

    @PreAuthorize("hasAuthority('/admin/delete')")
    @GetMapping("/admin/delete")
    public String adminDelete() {
        return "admin delete";
    }

}

以上在方法的声明之前添加@PreAuthorize注解,表示执行该方法之前需要授权,注解参数中的hasAuthority是固定的名称,括号内的/admin/list/admin/delete就是权限标识,此前配置的用户信息中有/admin/list,所以,该用户登录后可以访问以上测试控制器中的第1个路径,但是,无权访问第2个路径,当尝试访问第2个路径时,会出现403错误,表示“权限不足”!

当然,也可以在配置用户信息时,为用户添加若干个权限,例如:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 在内存中授权
    auth.inMemoryAuthentication()
            // withUser > 指定用户名
            .withUser("root")
            // password > 指定密码
            .password("1234")
            // authorities > 指定授权,方法参数值是权限标识字符串
            .authorities("/admin/list", "/admin/delete");
}

1.8. 关于授权-2

在演示第2种授权的做法之前,应该先将以上案例中的代码使之失效!先将类改名,并在类的声明之前添加@Deprecated注解,表示“声明为已过期”,同时,将关键的注解注释掉,则当前类就缺失了关键注解,导致项目不会自动使用当前类的各种配置!
在这里插入图片描述
TestController中,去掉原有的@PreAuthorize注解,添加更多的请求路径,以便于演示效果:


```java
@RestController
public class TestController {

    @GetMapping("/admin/list")
    public String adminList() {
        return "admin list";
    }

    @GetMapping("/admin/delete")
    public String adminDelete() {
        return "admin delete";
    }
    
    @GetMapping("/user/list")
    public String userList() {
        return "user list";
    }
    
    @GetMapping("/user/delete")
    public String userDelete() {
        return "user delete";
    }

}

由于此前演示的配置类已经作废,需要重新创建配置类,用于指定用户登录的账号及权限,并且,由于以上控制器类中的方法没有@PreAuthorize注解了,授权过程也应该在新的配置类中完成:

package cn.tedu.straw.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 当前方法的主要作用是:授权
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("1234")
                .authorities("admin_list", "admin_delete", "user")
                .and()
                .withUser("user")
                .password("1234")
                .authorities("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 当前方法的主要作用是:访问控制
        http.authorizeRequests()
                // 使用antMatchers()配置需要管理权限的URL,可以使用通配符,?表示任何1个字符,*表示任何1层路径资源,**表示任何层次的资源
                // 例如:配置为 /user/* 可以匹配 /user/delete、/user/list,却不可以匹配 /user/2020/list
                // 例如:配置为 /user/** 可以配置 /user/delete、/user/list、/user/2020/list、/user/2020/08/list
                // 紧随其后,使用hasAuthority()配置权限标识
                .antMatchers("/admin/list").hasAuthority("admin_list")
                .antMatchers("/admin/delete").hasAuthority("admin_delete")
                .antMatchers("/user/**").hasAuthority("user")
                // 对任何请求进行授权检查
                .anyRequest().authenticated();
        // 验证权限时,是使用登录表单进行授权的
        http.formLogin();
    }

}

在演示案例中,当需要退出登录时,可以在浏览器输入 http://localhost:8080/logout 以退出登录并切换账号进行测试。

以上2种做法各有优点,在“授权-1”的作法中,优点主要是对应关系非常明确,缺点在于不可以使用通配符,在“授权-2”的做法,优点主要在于可以集中管理权限(代码都写在同一个方法中),并可以使用路径中的通配符,但是,也因为管理很集中,就导致在控制器的代码中,权限检查并不直观。

1.9. 在密文之前添加前缀标识加密时使用的算法

在Spring Security处理密码时,可以不指定密码加密器(去除自动装配的任何密码加密器),然后,在密文之前使用{}框住密码加密器的id,例如:

{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO

则Spring Security在处理密码时,就会自动根据{bcrypt}对应的密码加密器(BCryptPasswordEncoder)来处理密码!

完整示例如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 当前方法的主要作用是:授权
    auth.inMemoryAuthentication()
            .withUser("admin")
            .password("{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
            .authorities("admin_list", "admin_delete", "user")
            .and()
            .withUser("user")
            .password("{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
            .authorities("user");
}

当然,以上代码等效于(以下代码显式的指定了密码加密器,同时密文没有指定密码加密器的id):

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

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 当前方法的主要作用是:授权
    auth.inMemoryAuthentication()
            .withUser("admin")
            .password("$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
            .authorities("admin_list", "admin_delete", "user")
            .and()
            .withUser("user")
            .password("$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
            .authorities("user");
}

1.10. 基于UserDetails的用户登录

以上的做法中,都是将用户的用户名、密码记录在配置类中,而配置类是整个项目启动之初就会被加载的,所以,这种做法无法满足动态用户名、密码的验证,在实际案例中,用户名、密码都应该是从数据库中读取出来的,不可能在项目启动之初就读取所有用户的用户名、密码并作为配置信息!

在Spring Security中,定义了一个UserDetailsService的接口,可以自定义类实现该接口:

public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return null;
    }
    
}

在整个登录过程中,Spring Security可以自动调用以上接口实现类中的loadUserByUsername()方法,并给出登录时的用户名,要求开发人员返回该用户名匹配的用户信息UserDetails,至少需要包括用户的密码,然后,Spring Security就可以根据返回信息中所包含的密码进行密码的验证,在整个操作过程中,Spring Security既不会给出用户此次登录的原始密码,也不要求以上方法返回的UserDetails中包含原始密码,而是全程自动处理!

演示代码示例:

package cn.tedu.straw.api.security;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 假设仅chengheng是正确的用户名
        if ("chengheng".equals(username)) {
            // 用户名是chengheng,则返回该用户的信息,后续Spring Security将根据返回的信息完成登录验证及授权
            UserDetails userDetails = User.builder()
                    .username("chengheng")
                    .password("{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
                    .authorities("user")
                    .build();
            return userDetails;
        }
        // 用户名不是root,则返回null,表示”无此用户“
        return null;
    }

}

然后,还需要在WebSecurityConfigureAdapter的子类中重写configureation(AuthenticationManagerBuilder auth)方法:

@Resource
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 当前方法的主要作用是:登录验证,授权
    auth.userDetailsService(userDetailsService);
}

2. 注册时对密码进行加密处理

由于使用了Spring Security框架,无论请求哪个路径,都是需要事先登录的,这个设计是不合理的,会在后续开发“登录”时再解决!

目前,在业务层中,尚未对用户注册时填写的密码进行加密,则需要修改“学生注册”的业务,在处理过程中,取出用户提交的原始密码,基于原始密码执行加密处理,然后将密文存储到数据库中!

为了避免在业务层使用Spring Security包中的工具类,导致后续各层代码之间的耦合度较高、多层代码都需要依赖于Spring Security,应该将“执行密码加密”的功能封装到专门的类中,使得业务层并不直接依赖于Spring Security!所以,在util包(需创建)中创建PasswordUtils工具类,在工具类添加“执行密码加密”的方法:

package cn.tedu.straw.api.util;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 密码工具类
 */
public class PasswordUtils {

    /**
     * 密码加密器
     */
    private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    /**
     * 执行密码加密
     *
     * @param rawPassword 原密码
     * @return 加密后得到的密文
     */
    public static String encode(String rawPassword) {
        return "{bcrypt}" + passwordEncoder.encode(rawPassword);
    }

}
```然后,在`UserServiceImpl`的`regStudent()`方法补充:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200826124038548.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xldWtl,size_16,color_FFFFFF,t_70#pic_center)
完成后,由于修改了业务层代码,需要再次执行业务层测试,以保证刚才的调整是正确的!

当业务层测试通过后,还应该**再次重启项目**,在浏览器中通过 http://localhost:8080/api/v1/users/student/register?phone=13100135001&password=1234&inviteCode=JSD1912-876840&nickname=Hello 进行测试注册。



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值