作者 | BoCong-Deng


写在前面

开发Web应用,对页面的安全控制通常是必须的。比如:对于没有访问权限的用户需要转到登录表单页面。要实现访问控制的方法多种多样,可以通过Aop、拦截器实现,也可以通过框架实现,例如:Apache Shiro、Spring Security。我们这里要讲的Spring Security 就是一个Spring生态中关于安全方面的框架。它能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案。



默认认证用户名密码

项目pom.xml添加spring-boot-starter-security依赖

1
2 org.springframework.boot
3 spring-boot-starter-security
4

重启你的应用。再次打开页面,你讲看到一个登录页面



spring security存储用户密码 spring security 密码_spring

既然跳到了登录页面,那么这个时候我们就会想,这个登录的用户名以及密码是什么呢?让我们来从SpringBoot源码寻找一下。你搜一下输出日志,会看到下面一段输出:



spring security存储用户密码 spring security 密码_用户名_02

这段日志是UserDetailsServiceAutoConfiguration类里面的如下方法输出的:



spring security存储用户密码 spring security 密码_spring_03

通过上面的这个类,我们可以看出,是SecurityProperties这个Bean管理了用户名和密码。在SecurityProperties里面的一个内部静态类User类里面,管理了默认的认证的用户名与密码。代码如下

1@ConfigurationProperties(
2 prefix = "spring.security"
3)
4public class SecurityProperties {
5 public static final int BASIC_AUTH_ORDER = 2147483642;
6 public static final int IGNORED_ORDER = -2147483648;
7 public static final int DEFAULT_FILTER_ORDER = -100;
8 private final SecurityProperties.Filter filter = new SecurityProperties.Filter;
9 private SecurityProperties.User user = new SecurityProperties.User;
10
11 public SecurityProperties {
12 }
13
14 public SecurityProperties.User getUser {
15 return this.user;
16 }
17
18 public SecurityProperties.Filter getFilter {
19 return this.filter;
20 }
21
22 public static class User {
23 private String name = "user";
24 private String password = UUID.randomUUID.toString;
25 private List roles = new ArrayList;
26 private boolean passwordGenerated = true;
27
28 public User {
29 }
30
31 public String getName {
32 return this.name;
33 }
34
35 public void setName(String name) {
36 this.name = name;
37 }
38
39 public String getPassword {
40 return this.password;
41 }
42
43 public void setPassword(String password) {
44 if (StringUtils.hasLength(password)) {
45 this.passwordGenerated = false;
46 this.password = password;
47 }
48 }
49
50 public List getRoles {
51 return this.roles;
52 }
53
54 public void setRoles(List roles) {
55 this.roles = new ArrayList(roles);
56 }
57
58 public boolean isPasswordGenerated {
59 return this.passwordGenerated;
60 }
61 }
62
63 public static class Filter {
64 private int order = -100;
65 private Set dispatcherTypes;
66
67 public Filter {
68 this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
69 }
70
71 public int getOrder {
72 return this.order;
73 }
74
75 public void setOrder(int order) {
76 this.order = order;
77 }
78
79 public Set getDispatcherTypes {
80 return this.dispatcherTypes;
81 }
82
83 public void setDispatcherTypes(Set dispatcherTypes) {
84 this.dispatcherTypes = dispatcherTypes;
85 }
86 }
87}

综上所述,security默认的用户名是user, 默认密码是应用启动的时候,通过UUID算法随机生成的,默认的role是"USER"。当然,如果我们想简单改一下这个用户名密码,可以在application.properties配置你的用户名密码,例如



spring security存储用户密码 spring security 密码_登录页面_04

当然这只是一个初级的配置,更复杂的配置,可以分不用角色,在控制范围上,能够拦截到方法级别的权限控制。



内存用户名密码认证

在上面的内容,我们什么都没做,就添加了spring-boot-starter-security依赖,整个应用就有了默认的认证安全机制。下面,我们来定制用户名密码。写一个继承了 WebSecurityConfigurerAdapter的配置类,具体内容如下

1import org.springframework.context.annotation.Configuration;
2import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
3import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
4import org.springframework.security.config.annotation.web.builders.HttpSecurity;
5import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
6import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
7
8@Configuration
9@EnableWebSecurity
10@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
11public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
12 @Override
13 protected void configure(HttpSecurity http) throws Exception {
14 super.configure(http);
15 }
16
17 @Override
18 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
19 auth.inMemoryAuthentication
20 .passwordEncoder(new BCryptPasswordEncoder)
21 .withUser("admin")
22 .password(new BCryptPasswordEncoder.encode("1234567"))
23 .roles("USER");
24 }
25}

这里对上面的代码进行简要说明:

  • Spring security 5.0中新增了多种加密方式,也改变了默认的密码格式。需要修改一下configure中的代码,我们要将前端传过来的密码进行某种方式加密,Spring Security 官方推荐的是使用bcrypt加密方式。inMemoryAuthentication.passwordEncoder(new BCryptPasswordEncoder),这相当于登陆时用BCrypt加密方式对用户密码进行处理。以前的".password("123")" 变成了 “.password(new BCryptPasswordEncoder.encode("123"))”,这相当于对内存中的密码进行Bcrypt编码加密。如果比对时一致,说明密码正确,才允许登陆。
  • 通过 @EnableWebSecurity注解开启Spring Security的功能。使用@EnableGlobalMethodSecurity(prePostEnabled = true)这个注解,可以开启security的注解,我们可以在需要控制权限的方法上面使用@PreAuthorize,@PreFilter这些注解。
  • 继承 WebSecurityConfigurerAdapter 类,并重写它的方法来设置一些web安全的细节。我们结合@EnableWebSecurity注解和继承WebSecurityConfigurerAdapter,来给我们的系统加上基于web的安全机制。
  • 在configure(HttpSecurity http)方法里面,我们进入到源码中,就会看到默认的认证代码是:

spring security存储用户密码 spring security 密码_登录页面_05

从方法名我们基本可以看懂这些方法的功能。上面的那个默认的登录页面,就是SpringBoot默认的用户名密码认证的login页面。我们使用SpringBoot默认的配置super.configure(http),它通过 authorizeRequests 定义哪些URL需要被保护、哪些不需要被保护。默认配置是所有访问页面都需要认证,才可以访问。

  • 通过 formLogin 定义当需要用户登录时候,转到的登录页面。
  • configureGlobal(AuthenticationManagerBuilder auth) 方法,在内存中创建了一个用户,该用户的名称为root,密码为root,用户角色为USER。这个默认的登录页面是怎么冒出来的呢?是的,SpringBoot内置的,SpringBoot甚至给我们做好了一个极简的登录页面。这个登录页面是通过Filter实现的。具体的实现类是org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter。同时,这个DefaultLoginPageGeneratingFilter也是SpringBoot的默认内置的Filter。

输入用户名,密码,点击Login。不过,我们发现,SpringBoot应用的启动日志还是打印了如下一段:



spring security存储用户密码 spring security 密码_spring_06

但实际上,已经使用了我们定制的用户名密码了。如果我们要配置多个用户,多个角色,可参考使用如下示例的代码:

1@Override
2 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
3 auth.inMemoryAuthentication
4 .passwordEncoder(new BCryptPasswordEncoder)
5 .withUser("admin")
6 .password(new BCryptPasswordEncoder.encode("1234567"))
7 .roles("USER")
8 .and
9 .withUser("admin1")
10 .password(new BCryptPasswordEncoder.encode("123"))
11 .roles("ADMIN