概述
Spring Security是为Java应用程序提供身份验证和授权 。
一般Web应用的需要进行认证和授权。
认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。
? 授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作(访问某个controller层的方法)。
关键接口
UserDetailsService
可以看做是根据用户名查询数据库或者从内存总获取密码,权限,角色等信息。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
PasswordEncoder
进行密码加密和匹配
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
在passwordEncoder的实现类中,BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器。BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过在构造函数中传入strength 控制加密强度,默认 10。
strength的范围在[4,31],可以在BCryptPasswordEncoder 中可以看到,不同springboot有可能不同。
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.version = version;
this.strength = strength == -1 ? 10 : strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
设置用户名和密码
- application.yaml
server:
port: 9000
spring:
security:
user:
name: user
password: 123
这种环境下不能配置WebSecurityConfigurerAdapter的实现类 否则配置用户名和密码无效
2. 在内存中设置
spring security中默认登录拦截uri是/login
package com.kj.config;
import org.springframework.beans.factory.annotation.Autowired;
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 注意这里需要密码需要加密 同时需要设置roles
// 否则会抛出异常 java.lang.IllegalArgumentException: Cannot pass a null GrantedAuthority collection
auth.inMemoryAuthentication().withUser("root").password(passwordEncoder.encode("123")).roles("");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启表单登录验证
http.formLogin();
// 这里需要设置处理login接口之外的其他请求需要认证
http.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated();
// 暂时关闭csrf 否则在登录是需要传递其他参数
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
这里csrf默认开启,同时springsecurity默认不开启认证,所有需要针对除了login接口外的其他接口都需要认证。当然,对于某些公共的静态资源这里也可以设置,为了演示简化了设置。
也可以同时设置多个内容中的用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("admin").password(passwordEncoder.encode("123")).roles("admin").build());
inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder.encode("456")).roles("root").build());
auth.userDetailsService(inMemoryUserDetailsManager).passwordEncoder(passwordEncoder);
}
3.自定义UserDetailService
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.ArrayList;
import java.util.List;
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 查询数据库 获取用户的密码,角色,权限等信息 填充
String pwd="";
String[] roles;
String[] authorities;
List<GrantedAuthority> auths=new ArrayList<>();
for (String role : roles) {
auths.add(new SimpleGrantedAuthority("ROLE_"+role);
}
for (String authority : authorities) {
auths.add(new SimpleGrantedAuthority(authority));
}
return new User(s,pwd,auths);
}
}
WebSecurityConfigurerAdapter配置自定义的UserDetailService
import org.springframework.beans.factory.annotation.Autowired;
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
http.authorizeRequests().antMatchers("/login").permitAll();
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
角色和权限
角色和权限也是 Spring Security 中所采用的授权模型。GrantedAuthority 对象代表的就是一种权限对象,是一个接口,它既可以看做是权限,同时也可以看做是角色,需要添加ROLE_前缀。代码如下:
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
创建权限
对于root用户具有了create,delete权限
UserDetails user = User.withUsername("root")
.password("123456")
.authorities("create", "delete")
.build();
创建角色
UserDetails user = User.withUsername("yn")
.password("123456")
.authorities("ROLE_ADMIN")
.build();
创建角色和权限
List<GrantedAuthority> auths= AuthorityUtils.commaSeparatedStringToAuthorityList("test,ROLE_normal");
User user=new User("test",passwordEncoder.encode("789"),auths);
角色继承
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。如果有多个\n进行分割
实际中可以考虑使用权限组的形式来代替这种继承
角色和权限校验
api | 作用 |
anonymous | 允许匿名访问 |
authenticated | 允许认证用户访问 |
denyAll | 无条件禁止一切访问 |
hasAnyAuthority | 允许具有任一权限的用户进行访问 |
hasAnyRole | 允许具有任一角色的用户进行访问 |
hasAuthority | 允许具有特定权限的用户进行访问 |
hasIpAddress | 允许来自特定 IP 地址的用户进行访问 |
hasRole | 允许具有特定角色的用户进行访问 |
permitAll | 无条件允许一切访问 |
api式校验
eg:
@Override
protected void configure(HttpSecurity http) throws Exception
http.formLogin();
// 这里Role不用加前缀
http.authorizeRequests().antMatchers("/login").permitAll()
.antMatchers("/port").hasRole("admin");//
http.csrf().disable();
}
注解校验
@Secured
只能用于权限用户具有某个权限(角色(需要加前缀)或者权限)才能访问该方法,首先需要在主启动类上开启(),然后在对应controller方法上开启
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class,args);
}
}
@GetMapping("/port")
@Secured("ROLE_root")
public String getPort(){
return this.port;
}
PreAuthorize
方法之前检验
主启动类上添加@EnableGlobalMethodSecurity(prePostEnabled =true)
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class,args);
}
}
@GetMapping("/port")
@PreAuthorize("hasRole('root')")
public String getPort(){
return this.port;
}
PostAuthroize
方法之后校验
主启动类上添加@EnableGlobalMethodSecurity(prePostEnabled =true),内部调用之前的api
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class,args);
}
}
@GetMapping("/port")
@PostAuthorize("hasRole('root')")
public String getPort(){
return this.port;
}
PreFilter
进入控制器之前对数据进行过滤,要求传入的必须是collection或者是数组 ,迭代时元素别名为filterObject
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class,args);
}
}
//student中id为偶数的保留
@GetMapping("/port")
@PreFilter(value = "filteObject.id%2==0")
public String getPort(List<Student> students){
return this.port;
}
PostFilter
权限验证之后对数据进行过滤
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class,args);
}
}
//student中id为偶数的返回
@GetMapping("/port")
@PreFilter(value = "filteObject.id%2==0")
public List<Student> getPort(){
return null;
}
登出
退出登录后需要设置清除cookie,session,认证信息 这些都是默认的,可以不用配置。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
http.authorizeRequests().antMatchers("/login").permitAll();
//设置退出登录的接口以及方法,默认是get,同时可以设置登出成功回调
http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "DELETE"))
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 前后端分离 中可以通过HttpServletResponse返回 json信息
}
});
http.csrf().disable();
}
自定义访问界面
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 自定义登陆界面和成功后的页面
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.loginPage("/index")
.loginProcessingUrl("/login") //和表单里的action属性要一样 表单一定是post请求
.defaultSuccessUrl("/success") //设置了成功和失败的URL 就跳转到原来要访问的地址
.failureForwardUrl("/fail")
.passwordParameter("pwd") //指定表单里的属性名
.usernameParameter("uname");
http.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated();
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String encode = passwordEncoder().encode("123");
auth.inMemoryAuthentication().withUser("user").password(encode).roles("admin");
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
登录成功和失败的URL
在 Spring Security 中,和登录成功重定向 URL 相关的方法有两个:
- defaultSuccessUrl(url)(如果一开始访问的是登录接口,会跳转到url中,否则一开始访问的是其他接口,就会跳转到对应的接口)
- successForwardUrl(url) (不管登录前访问的是什么接口,登录成功后都跳转到指定的URL)
失败的URL
与登录成功相似,登录失败也是有两个方法:
- failureForwardUrl
- failureUrl
这两个方法在设置的时候也是设置一个即可。failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,会发生重定向。
在前后端分离的项目中,页面调整是由前端控制的,相对来说用的少。
一些回调接口
接口
常用的一些扩展接口
- AuthenticationSuccessHandler(登录成功处理)
- AuthenticationFailureHandler(登录失败处理)
- logoutSuccessHandler(注销成功处理)
- logHandler(注销处理)
- AccessDeniedHandler(访问拒绝处理)
- AuthenticationEntryPoint(身份验证入口点失败处理)
AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
http.authorizeRequests().antMatchers("/login").permitAll();
http.formLogin().successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 登录成功对调
}
});
http.formLogin().failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 登录失败回调
}
});
http.logout().logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 登出成功回调
}
});
// 登出处理
http.logout().addLogoutHandler(...)
http.csrf().disable();
}
csrf
csrf是指用户对浏览器的信任,第三方,利用一些技术手段,欺骗用户访问,认证过的链接,认证过,就会携带对应的cookie,对用户恶意攻击。
解决方案之一
令牌同步模式
这是目前主流的CSRF 攻击防御方案。具体的操作方式就是在每一个HTTP请求中,除了默认自动携带的Cookie 参数之外,再提供一个安全的、随机生成的宇符串,我们称之为CSRF令牌。这个CSRF令牌由服务端生成,生成后在HtpSession中保存一份。当前端请求到达后,将请求携带的CSRF令牌信息和服务端中保存的令牌进行对比,如果两者不相等,则拒绝掉该HITTP请求。
注意:考虑到会有一些外部站点链接到我们的网站,所以我们要求请求是幂等的,这样对子HEAD、OPTIONS、TRACE等方法就没有必要使用CSRF令牌了,强行使用可能会导致令牌泄露!
spring security默认开启csrf,不用配置。
前后端分离项目中,一开始应该发送两次请求,第一次会获取csrf令牌 保存在cookie里,之后按照一定方式组装csrf令牌,然后提交。
cookie里key: XSRF-TOKEN
前后端分离的组装方式
value为token值
1 json格式请求
//请求题里添加
_csrf:value
2 http-header中添加
//value从cookie中获取
X-XSRF-TOKEN:value
remember-me
基本配置
记住我的功能实现思路基本是基于token,同时要考虑token是否过期的情况,以及是否更新。
spring security里的token里保留了用户名,过期时间,还有其他的一些信息,返回的是经过Base64编码后的token。
token的生成需要key,key 默认值是一个 UUID 字符串,这样会带来一个问题,就是如果服务端重启,这个 key 会变,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效,所以,我们可以指定这个 key
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("salt")
.and()
.csrf().disable();
}
如果要通过表单传递rememberMe key为remember-me
<div> Remember Me:<input type="checkbox" name="remember-me" value="true"/> </div>
token持久化
token存储默认是基于内存的,服务器如果重启,信息就会丢失。token需要持久化在数据库里。
保存Token立牌的类是PersistentRememberMeToken
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;
}
对应的SQL脚本为
CREATE TABLE `persistent_logins`
`username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
有关存储的类是JdbcTokenRepositoryImpl
相关的配置
数据库
引入mysql的驱动和在application.yaml里配置数据库地址
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.24</version>
</dependency>
application.yaml
server:
port: 9000
spring:
datasource:
url: jdbc:mysql://127.0.0.1/sec?serverTimezone=UTC&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
security配置
import org.springframework.beans.factory.annotation.Autowired;
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import javax.sql.DataSource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
http.authorizeRequests().antMatchers("/login").permitAll();
http.rememberMe().key("salt").tokenRepository(jdbcTokenRepository());
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
二次校验
为了让用户使用方便,我们开通了自动登录功能,但是自动登录功能又带来了安全风险,一个规避的办法就是如果用户使用了自动登录功能,我们可以只让他做一些常规的不敏感操作,例如数据浏览、查看,但是不允许他做任何修改、删除操作,如果用户点击了修改、删除按钮,我们可以跳转回登录页面,让用户重新输入密码确认身份,然后再允许他执行敏感操作。
例如我现在提供三个访问接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin")
public String admin() {
return "admin";
}
@GetMapping("/rememberme")
public String rememberme() {
return "rememberme";
}
}
- 第一个 /hello 接口,只要认证后就可以访问,无论是通过用户名密码认证还是通过自动登录认证,只要认证了,就可以访问。
- 第二个 /admin 接口,必须要用户名密码认证之后才能访问,如果用户是通过自动登录认证的,则必须重新输入用户名密码才能访问该接口。
- 第三个 /rememberme 接口,必须是通过自动登录认证后才能访问,如果用户是通过用户名/密码认证的,则无法访问该接口。
接口的访问配置如下
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/rememberme").rememberMe()
.antMatchers("/admin").fullyAuthenticated()
.anyRequest().authenticated()
.and()
.formLogin();
}