项目地址:https://github.com/AganRun/SpringInAction/tree/master/Chapter4/taco-security
第四章 配置Spring Security
初次使用
当加入了starter启动器后,项目启动时,在日志中会出现一个账号及随机密码
Using generated security password: cf18a7e9-ffa6-429f-9331-40d2dd121f53
此时登录项目会有一个登录页面,输入账号密码才能访问
security starter的影响
- 所有HTTP请求都需要认证,认证过程是通过HTTP basic认证对话框实现的
- 没有特定的角色及权限,只有一个user用户
- 没有登录页面
配置Security
只有一个用户显然满足不了需求,security有四种配置用户的方式
- 基于内存的用户存储 (将账号密码直接硬编码到配置代码中,优点:方便快捷,可以用来调试。缺点:项目上线后不方便修改用户)
- 基于JDBC的用户存储
- 以LDAP作为后端的用户存储
- 自定义用户详情服务
JDBC配置Security
当配置了数据源之后,Spring有一套默认的用户搜索SQL。若表于其不匹配,则可以自定义SQL去配置用户。
酱默认的SQL查询替换为自定义的设计时,很重要的一点是遵循查询的基本协议。所有查询将用户名作为唯一参数
@Configuration
@EnableWebSecurity
public class SecurityJdbcConfig extends WebSecurityConfigurerAdapter {
@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()方法可以接受任意的PasswordEncoder接口的实现
- BCryptPasswordEncoder: 使用bcrypt强哈希加密
- NoOpPasswordEncoder: 不进行任何转码(已过期)
- Pbkdf2PasswordEncoder: 使用PBKDF2加密
- SCryptPasswordEncoder: 使用scrypt哈希加密
- StandardPasswordEncoder: 使用SHA-256哈希加密
逻辑是:数据库中的密码应该永远不会被解密。将输入的密码进行算法转码,然后与库中的密码对比。
@Bean
public static PasswordEncoder passwordEncoder() {
//自定义一个加密流程
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
//加1加密,哈哈
System.out.println("encode" + charSequence);
return charSequence.toString() + 1;
}
@Override
public boolean matches(CharSequence charSequence, String s) {
System.out.println("match,charSequence:" + charSequence + ", dbPassword:" + s);
return encode(charSequence).equals(s);
}
};
}
DB: user1/12341
输入: user1/1234
控制台输出
match,charSequence:1234, dbPassword12341
encode1234
自定义用户认证
- 自定义一个实体类,例如User,去实现UserDetails接口
- 定义对应的Repository,Service(需要实现UserDetailsService的loadUserByUsername(String username))
- 配置类中,auth.userDetailsService(注入的service)
保护Web请求
在WebSecurityConfigureAdapter的Configure(HttpSecurity http)方法中
可以配置的功能有:
- 为某个请求提供服务之前,先预先满足条件
- 配置自定义的登录页
- 支持用户退出应用
- 预防跨站请求伪造
保护请求 & 登录
可以通过配置路径与对应的角色控制,并指定登录页
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
// .hasRole("USER")
.access("hasRole('USER')") // design 和 orders 路径,需要有user角色
.antMatchers("/", "/**")
// .permitAll();
.access("permitAll") //对于/和/**路径不拦截
.and()
.formLogin()
.loginPage("/login") //登录页面
.defaultSuccessUrl("/design") //成功后重定向到design页面
;
}
存在很多的配置方法,列举其中的几个
方法 | 能做什么 |
---|---|
access(String) | 如果给定的SpEL表达式计算结果为TRUE,就允许访问 |
authenticated() | 允许认证过的用户访问 |
denyAll() | 无条件拒绝所有访问 |
hasIpAddress(String) | 如果请求来自给定的IP地址,允许访问 |
hasRole(String) | 如果用户具备指定的角色,允许访问 |
not() | 对其他方法的结果求反 |
permitAll() | 无条件允许访问 |
rememberMe() | 如果用户通过Remember-me认证的,允许访问 |
SpEl表达式可以编写复杂的逻辑。例如只允许具备ROLE_USER权限的用户,在星期二下午创建新的Taco
退出
.and().logout().logoutSuccessUrl("/");
POST请求403?
在上述配置完之后,即使用户已经登录了,在提交订单,请求/design时依旧会被拦截,页面403。
那是因为SpringSecurity默认是开启了内置的CSRF保护, 建议不要关掉。先了解一下什么是CSRF。
CSRF
跨站请求伪造(Cross-Site Request Forgery, CSRF)。
它会让用户在一个恶意Web页面填写信息,然后自动将表单以攻击受害者的身份提交到另外一个应用上。
可以理解为:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
预防措施 :应用在展现表单的时候,生成一个CSRF token,放在隐藏域存储起来,在提交表单的时候将token一起发送至服务器,由服务器对比匹配。
可以加入隐藏域(JSP、Thymeleaf默认生成)
提交表单时,使用th:action属性
了解用户是谁
如何获取当前用户的信息
- 注入Principal对象到控制器
- 注入Authentication对象到控制器
- 使用@AuthenticationPricipal注解标注方法
- 使用SecurityContextHolder来获取安全上下文
public String processDesign(@Valid Taco taco, Errors errors, @ModelAttribute Order order, Principal principal) {
String username = principal.getName() //获取用户的username=>获取用户信息 缺点:业务中会掺杂安全代码
public String processDesign(@Valid Taco taco, Errors errors, @ModelAttribute Order order, Authentication authentication) {
User user = (User) authentication.getPrincipal(); //强转User对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal(); //繁琐,但是任何地方都可以,不限制在Controller中
public String processDesign(@Valid Taco taco, Errors errors, @ModelAttribute Order order, @AuthenticationPrincipal User user) {
//使用注解,最方便
}