启用Spring Security
- 添加依赖
- spring-boot-starter-security
- 只要添加了该依赖,自动配置功能就会探测到Spring Security出现在了类路径中,因此它就会初始化一些基本的安全配置
- 所有的HTTp请求路径都需要认证;
- 不需要特定的角色和权限;
- 没有登陆页面;
- 认证过程通过HTTP basic认证对话框实现;
- 系统只有一个用户,用户名为user。
- 目标配置
- 通过登陆页面来提示用户进行认证,而不是使用HTTP basic对话框;
- 提供多个用户,并提供一个注册页面;
- 对不同的请求路径,执行不同的安全规则,举例来说,主页和注册页面根本不需要进行认证。
配置Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
Spring Security为配置用户存储提供了多个可选方案:
- 基于内存的用户存储;
- 基于JDBC的用户存储;
- 以LDAP作为后端的用户存储;
- 自定义用户详情服务。
不管使用哪种,都可以通过覆盖 WebSecurityConfigurerAdapter基础配置类中定义的configure()方法来进行配置。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
基于内存的用户存储
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("buzz")
.password("infinity")
.authorities("ROLE_USER")
.and()
.withUser("woody")
.password("bullseye")
.authorities("ROLE_USER");
}
- 对于测试和简单的应用来说,基于内存的用户存储是很有用的,但是这种方式不能很好的编辑用户
基于JDBC的用户存储
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username,password,enabled from Users where username=?"
)
.authoritiesByUsernameQuery(
"select username,authority from UserAuthorities where username=?"
);
}
- 上述的认证查询,它预期使用了明文密码存储,这样是有风险的。下面我们需要借助passwordEncoder()方法指定一个密码转码器(encoder);
- .passwordEncoder(new StandardPasswordEncoder("53cr3t");
- passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意实现。Spring Security的加密模块包括了多个这样的实现:
- BCryptPasswordEncoder:使用bcrypt强哈希加密。
- NoOpPasswordEncoder:不进行任何转码。
- Pbkdf2PasswordEncoder:使用PBKDF2进行加密。
- SCryptPasswordEncoder:使用scrypt哈希加密。
- StandardPasswordEncoder:使用SHA-256哈希加密。
- 不管使用哪一个密码转码器,都需要理解一点,即数据库中的密码是永远不会解码的。
- 用户在登录时所采取的策略与之相反,输入的密码会按照相同的算法进行转码,然后与数据库中已经转码过的密码进行对比。(这个对比是在PasswordEncoder的matches()方法中进行的。
以LDAP作为后端的用户存储
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}");
}
-
userSearchFilter()和groupSearchFilter():为基础LDAP查询提供过滤条件,分别用于搜索用户和组。
-
user/groupSearchBase():为查找用户/组提供了基础查询
- 参数ou=people/groups:声明了用户/组应该在名为people/groups的组织单元下开始搜索。
-
密码策略:
- .passwordCompare():
- .passwordEncoder(new BCryptPasswordEncoder())
- .passwordAttribute("passcode")
...
自定义用户认证
@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- UserDetails:通过实现该接口,我们能够提供能多信息给框架,比如用户都被赋予了哪些权限以及用户的账号是否可用。
- is...Expired():各种方法都会返回一个boolean值,表明用户的账号是否可用或过期。
- getAuthorities():该方法返回用户被授予权限的一个集合。
- 对于User来说,getAuthorities()方法只是简单的返回了一个集合,这个集合表明所有的用户都被授予了ROLE_USER权限
public interface UserRepository extends CrudRepository<User,Long> {
User findByUsername(String username);
}
创建用户详情服务
public interface UserDetailsService{
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
- UserDetailsService:该接口是Spring Security提供的一个相当简单直接的接口
- 该接口的实现会得到一个用户的用户名,并且要么返回查找到的UserDetails对象,要么在根据用户名无法得到任何结果的情况下返回UsernameNotFoundException。
@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
private UserRepository userRepo;
@Autowired
public UserRepositoryUserDetailsService(UserRepository userRepo) {
this.userRepo = userRepo;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepo.findByUsername(s);
if (user != null) {
return user;
}
throw new UsernameNotFoundException("User '" + s + "'' not found");
}
}
- UserRepository通过构造器被注入
- loadByUsername()方法有一个简单的规则:它绝不能返回null
- @Service:这是Spring的另一个构造型(stereotype)注解,它表明这个类要包含到Spring的组件扫描中
回到configure()方法
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
@Bean
public PasswordEncoder encoder() {
return new StandardPasswordEncoder("53cr3t");
}
- 这里,我们只是简单地调用userDetailService()方法,并将自动装配到SecurityConfig中的UserDetailsService实例传递了进去。
注册用户
@Controller
@RequestMapping("/register")
public class RegistrationController {
private UserRepository userRepo;
private PasswordEncoder passwordEncoder;
public RegistrationController(UserRepository userRepo, PasswordEncoder passwordEncoder) {
this.userRepo = userRepo;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
public String registerForm(){
return "registration";
}
@PostMapping
public String processRegistration(RegistrationForm form){
userRepo.save(form.toUser(passwordEncoder));
return "redirect:/login";
}
}
保护Web请求
为了配置一些安全性规则,下面介绍一个WebSecurityConfigurerAdapter的其他configure()方法。
@Override
protected void configure(HttpSecurity http) throws Exception{
...
}
configure()方法接受一个HttpSecurity对象,能够用来配置在web级别该如何处理安全性。我们可以使用HttpSecurity配置的功能包括
- 在为某个请求提供服务之前,需要预先满足特定的条件;
- 配置自定义的登陆页;
- 支持用户退出应用;
- 预防跨站请求伪造。
保护请求
我们需要确保只有认证过的用户才能发起对“/design”和“/orders”对请求,而其他请求对其他用户均可用。如下配置即可
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**")
.permitAll();
}
- 定义了两条规则
- 具备ROLE_USER权限的用户才能访问“/design”和“/orders”
- 其他的请求允许所有用户访问
- 声明在前面的安全规则优先级更高
- 一些其他的方法
| 方法 | 能够做什么 |
|---|---|
| access(String) | 如果给定的SqlEl表达式计算结果为true,就允许反问 |
| anonymous() | 允许匿名用户访问 |
| authenticated() | 允许认证过的用户访问 |
| denyAll() | 无条件拒绝所有访问 |
| fullyAuthenticated() | 如果用户是完整认证的,就允许访问 |
| hasAnyAuthority(String...) | 如果用户具备给定权限中的某一个,就允许访问 |
| hasAnyRole(String...) | 如果用户具备给定权限中的某一个,就允许访问 |
| hasAuthority(String) | 如果用户具备给定权限,就允许访问 |
| hasIpAddress(String) | 如果请求来自给定IP地址,就允许访问 |
| hasRole(String) | 如果用户具备给定角色,就允许访问 |
| not() | 对其他访问方法的结果求反 |
| permitAll() | 无条件允许访问 |
| rememberMe() | 如果用户是通过Remember-me功能认证的,就允许访问 |
创建自定义的登陆页
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers("/", "/**")
.access("permitAll")
.and()
.formLogin()
.loginPage("/login")
;
}
- 在configure里配置formLogin来指明路径,之后可以为“/login”路径addViewController
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")
.defaultSuccessUrl("/design",true)
- 这里,声明Spring Security要监听对“/authenticate”对请求来处理登录信息的提交
- 同时,用户名和密码的字段名应该是user和pwd
- defaultSuccessUrl("/design",true)
- 默认情况下,登录成功后会被定向至/design
- 第二个参数true意味:强制用户在登录成功之后统一访问design页面,即使用户在登录之前正在访问其他页面,在登录之后也会被定向到design。
退出
.and()
.logout()
.logoutSuccessUrl("/")
- 该配置会搭建一个安全过滤器,该过滤器会拦截对“/logout”的请求。
- 当请求/logout时,它们的session会被清理掉,这样它们就能退出应用。
- logoutSuccessUrl可以指定推出后的不同页面。
防止跨站请求伪造
了解用户是谁
@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializeble{
...
@ManyToOne
private User user;
...
}
- ManyToOne:表示用户和订单一对多的关系
我们有多种方式来确定用户是谁
- 注入Principal对象到控制器方法中;
- principal.getName()
- 注入Authentication对象到控制器方法中;
- (User)authentication.getPrincipal()
- 使用SecurityContextHolder来获取上下文;
- 使用@AuthenticationPrincipal注解来标注方法。
- @AuthenticationPrincipal User user
小结
- Spring Security的自动配置是实现基本安全性功能的好办法,但是大多数应用都需要显式的安全配置,这样才能够满足特定的安全需求。
- 用户详情可以通过用户存储进行管理,它的后端可以是关系型数据库、LDAP或完全自定义实现。
- Spring Security会自动防范CSRF攻击。
- 已认证用户的信息可以通过SecurityContext对象(该对象可由SecurityContextHolder.getContext()返回)来获取,也可以借助@AuthenticationPrincipal注解将其注入到控制器中。