本文内容适合刚接触spring security的新手,大神请跳过。spring security是一个用来保护spring应用程序的框架,它在用户访问web程序的时候会进行身份的认证(判断当前用户是谁)和授权(当前用户能访问哪些uri,不能访问哪些uri)。我们经常见到场景:1.访问某些网站时需要先登录用户名和密码;2.当你用自己的用户名密码登录某电商网站后,你只能浏览自己的订单,不能看别人的,而电商的某些运营人员或工程师可以看到所有用用户的订单;这个就是认证和授权。接下来我们结合一个Demo来具体看下认证和授权到底是个什么鬼,以及整个过程是怎么操作的(其实只需要重写三个方法就行了)。Demo源码
场景介绍:我们有一个controller,它包含两个方法,一个用来创建用户,另一个用来获取用户。uri如下:
创建用户:/users
获取用户:/users/{username}
当我们在building.gradle
文件中添加spring security的依赖时默认security自动开启保护。它做了两件事情,1.访问所有的uri都需要通过认证,没有用过认证的用户不能访问uri,通过认证没有达到该uri访问权限的用户也不能访问该uri;2.spring security自动生成一个用户,它的用户名是user
,密码每次web服务启动时才会随机产生。如下图所示:
这种保护策略当然不是我们想要的,所以我们要自定义保护策略(地球人都是这么干的)。对于上面的两个uri我们规定:创建用户不需要认证和授权,获取用户的时候需要认证和授权。为了实现自定义策略,我们需要继承WebSecurityConfigurerAdapter
类,一看名字它就是专门用来配置spring security的。那好我们先来看下它默认的配置长什么样子。代码如下:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
这是WebSecurityConfigurerAdapter
中的一个方法,它控制每个uri的访问权限,目前默认的配置是每个请求都需要认证,系统支持表单认证和httpbasic两种认证方式。要实现我们的自定一就必须重写这个方法,代码如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/users")
.permitAll()
.antMatchers(HttpMethod.GET, "/users/*")
.hasRole("ADMIN")
.and()
.formLogin()
.and()
.httpBasic();
}
permitAll()
允许所有人访问/users
,而只有具有ADMIN
角色的用户才能访问/users/*
同样支持表单和httpBasic两种认证方式,这就是spring security的安全性配置。
我们能成功登录电商网站(认证通过)是因为网站里存储了们的账户名和密码,系统拿我们输入的用户名密码跟后台已存储的进行比对,若都一致则登录成功(认证通过)。而此时系统只有一个user用户,密码是随机的,那我们想让用户名:admin,密码:password的用户能通过认证并且能访问/users/{username}
该怎么办呢?只需要添加用户存储就可以了,spring security提供了基于内存的用户存储和基于数据库的用户存储两种方式。因为是Demo展示,所以本文选择基于内存的用户存储。仍旧是重写方法,代码如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin")
.password(new BCryptPasswordEncoder().encode("password"))
.roles("ADMIN")
.and()
.withUser("user")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("USER");
}
上述代码在内存中创建了两个用户,信息如下:
用户名 | 密码 | 角色 |
---|---|---|
admin | password | ADMIN |
user | 123 | USER |
此时在系统内部就已经存在了两个用户(当重写此方法后spring security就不会再默认生成用户了),当外部输入的用户跟二者一致能匹配上时,认证就通过了。
接下来就是最后一个问题了,认证过程是怎么执行的。当初这块也来回看了好几次,最后是根据别人些的博客外加源码打断调试才搞明白的。简单来说认证过程大多数工作系统代码已经帮我们做了,我们只需要关心UserDetails
和UserDetailsService
这两个都是接口,前者的实现类User
用来记录"找到的"用户的详细信息,后者里面只有一个方法loadUserByUsername
,这个方法是用来实现"找" 这个步骤的,接下来机进行详细解释。
UserDetailsService接口:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
因为Demo是基于内存的用户存储,所以我们来看UserDetailsService
的一个实现类InMemoryUserDetailsManager
中的loadUserByUsername
方法怎么重写的,代码如下:
public class InMemoryUserDetailsManager implements UserDetailsManager,
UserDetailsPasswordService {
private final Map<String, MutableUserDetails> users = new HashMap<>();
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
}
当应用程序启动时,内存中的那两个用户会用来初始化成员变量users
其中key是用户名,value是用户名对应的用户封装成MutableUserDetails
(MutableUser是UserDetails的一个实现类)也就是说系统的已存用户都在users
中。接着,用户提交的用户名和密码会被封装成Authentication
(这是个接口,大多情况下都用它的实现类UsernamePasswordAuthenticationToken
这个实现类有两个成员变量principal
和credentials
,前者用来存储用户名,后者存储密码)通过一系列的逻辑,Authentication
将principal
送到loadUserByUsername
方法中,没错,上面的入口参数username
就是principal
。之后就是根据username
在users中找有没有相同的,没有就报异常,有的话就将其封装成User
(这也是UserDetails
的一个实现类)返回。这就是上面说的"找到了",具体通没通过认证呢?后面还会有个方法再校验User
中的password,如果密码匹配则校验才算通过,不过密码校验系统已经替我们把代码写了。
上述是基于内存的的用户认证,所以直接就使用了InMemoryUserDetailsManager
,如果是基于mysql的用户认证,可以自己写一个类继承UserDetailsService
并实现loadUserByUsername
方法根据username
从mysql中读取数据再封装成User
。
Demo中是有一些测试:
测试方法:should_create_user_succeed()是创建用户,不需要认证和授权名,所以直接返回201。
测试方法:should_query_user_succeed()是获取用户, 因为是admin访问,所以请求成功,返回200。
测试方法:should_401_if_query_user_with_Unauthorized_user()是获取用户,因为密码错误,所以未通过认证,返回401。
测试方法:should_403_if_query_user_with_no_admin()是获取用户,认证通过了,但没有ADMIN角色,所以返回403。
就先写到着吧,spring security还有好几个东西,等有时间再一点点补齐。建议看下spring mvc中一个请求的执行过程,理解下拦截器和过滤器,会更加清楚spring security的执行过程。