目录
在java web工程中,一般使用Servlet过滤器(Filter)对于请求进行拦截,然后在Filter中通过自己的验证逻辑来决定是否放行请求。基于这一原理,常用的SpringMVC实现了自己的拦截器,同样的,Spring Security也是基于这个原理,在进入到DispatcherServlet前就可以对Spring MVC的请求进行拦截,然后通过一定的验证(一般验证用户是否有某个权限、请求类型、请求方式等),从而决定是否放行某个请求。
开始之前
1、Spring Security的主要功能就是通过一定的验证,从而决定是否放行某个请求,可以实现用户访问权限控制,HTTP和HTTPS访问控制、CSRF(跨站点请求伪造)访问控制等;
2、Spring Security的拦截会默认先于其它过滤器之前执行;
3、针对Spring Security,角色权限的命名强制都以"ROLE_"开头且全部大写,比如"ROLE_USER","ROLE_ADMIN",
"ROLE_DBA",当然,角色权限可以任意命名,甚至可以定义一个角色权限为"ROLE_HAHA","ROLE_WUDI"。其提供的方法有的会自动给角色权限加上"ROLE_",这时,就不能给角色权限加上"ROLE_",这里需要注意(后面会提供说明)。
快速开始
1、在Spring Boot项目中引入Spring Security的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、启动注解
官方定义的是:web工程使用@EnableWebSecurity,非web工程可以使用@EnableGlobalAuthentication。而事实上,@EnableWebSecurity已经标注了@EnableGlobalAuthentication(可以点进源码查看)。
所以,只需要在启动类上标注@EnableWebSecurity即可开启Spring Security功能。
3、启动Spring Boot项目
启动Spring Boot项目后,可以在Console里面看到随机生成的秘钥,如下。
随即在浏览器中任意输入一个存在于Spring Boot项目中的url,就会出现一个拦截页面,输入刚刚生成的秘钥即可访问到预期的页面,如下。
4、短板
上述过程暴露了一下问题:
-每次启动都会生成不同的秘钥,造成在访问的过程每次都要输入不同的秘钥,如果丢失又要重启,实在是不方便;
-用户只能使用‘user’账号,无法多样化,不适合构建不同的权限;
-不能自定义自己的验证方式和策略;
-验证界面不美观;
-不能定义哪些url需要验证,哪些不需要;
.....................
为了克服只能使用user+自动生成的秘钥引起的弊端,Spring Security提供了使用内存签名服务、数据库签名服务和自定义签名服务。
使用内存签名服务
顾名思义,就是将用户信息存放在内存中(实际项目都是在数据库中)。相对而言,它比较简单,适合于快速搭建测试环境。
1、继承WebSecurityConfigurerAdapter重写拦截配置
里面有三个常用方法供我们重写,如下。
@Configuration
public class RoleConfig extends WebSecurityConfigurerAdapter {
/**
* <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
super.configure(auth);
}
/**
* <p>用来配置Filter链
* */
@Override
public void configure(WebSecurity web) throws Exception {
// TODO Auto-generated method stub
super.configure(web);
}
/**
* <p>用来配置拦截保护请求,比如什么请求放行,什么请求需要验证
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
}
}
2、定义签名服务
根据上面所述,我们只需要重写下面的方法即可。
/**
* <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
super.configure(auth);
}
完整的示例
@Configuration
public class RoleConfig extends WebSecurityConfigurerAdapter {
/**
* <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码编码器-在Spring5的Security中都要求使用密码编码器,否则会发生异常
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//使用内存签名服务
auth.inMemoryAuthentication()
//设置密码编码器
.passwordEncoder(passwordEncoder)
//注册用户admin,密码为abc123,并且赋予USER和ADMIN的角色权限
.withUser("admin").password(passwordEncoder.encode("abc123")).roles("USER", "ADMIN")
//连接方法and()
.and()
//注册用户myuser,密码为123456,并且赋予USER角色权限
.withUser("myuser").password(passwordEncoder.encode("123456")).roles("USER");
//还需要注册其它用户继续用and()连接......
}
}
3、重新启动Spring Boot项目
可以发现Console中不再为我们打印秘钥了,随即在浏览器中输入任意一个Controller中存在的url,同样出现拦截页面,输入我们自定义注册的用户即可访问到预期的页面。
4、其它
user-detail机制其它构造方法
方法 | 描述 |
accountExpired(boolean accountExpired) | 设置账号是否过期 |
accountLocked(boolean accountLocked) | 是否锁定账号 |
credentialsExpired(boolean credentialsExpired) | 定义凭证是否过期 |
disabled(boolean disabled) | 是否禁用用户 |
username(String username) | 定义用户名,不能为null |
authorities(GrantedAuthority... authorities) | 赋予一个或多个权限,需要加上ROLE_ |
authorities(List<? extends GrantedAuthority> authorities) | 使用列表(List)赋予权限 |
password(String password) | 定义密码 |
roles(String... roles) | 赋予一个或多个权限,会自动加上ROLE_ |
提示:上面示例已经演示了username()、password()和roles()构造方法了,其它的依葫芦画瓢即可。
使用数据库签名服务
毕竟内存有限,且也不适合实际开发环境。因此Spring Security提供了对数据库的查询方法来满足需求。
1、创建数如下据表,并配置好数据源等其他配置
说明:user_available字段表示用户是否可用1-可用,0-不可用。
2、继承WebSecurityConfigurerAdapter重写拦截配置,定义签名服务
@Configuration
public class RoleConfig2 extends WebSecurityConfigurerAdapter {
//注入数据源
@Autowired
private DataSource dataSource;
//根据用户名查询用户信息
private final String getUserByUsername = "SELECT user_name as username, user_pwd as password, user_available as enabled "
+ "FROM tb_user "
+ "WHERE user_name = ?";
//根据用户名查询角色信息
private final String getRoleByUsername = "SELECT u.user_name as username, r.role_name as authority "
+ "FROM tb_user u, tb_user_role_mid ur, tb_role r "
+ "WHERE u.user_id = ur.user_id AND r.role_id = ur.role_id AND u.user_name = ?";
/**
* <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码编码器-在Spring5的Security中都要求使用密码编码器,否则会发生异常
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//使用数据库签名服务
auth.jdbcAuthentication()
//设置密码编码器
.passwordEncoder(passwordEncoder)
//数据源
.dataSource(dataSource)
//查询用户,自动判断密码是否一致
.usersByUsernameQuery(getUserByUsername)
//赋予权限
.authoritiesByUsernameQuery(getRoleByUsername);
}
}
3、重新启动Spring Boot项目
可以发现Console中不再为我们打印秘钥了,随即在浏览器中输入任意一个Controller中存在的url,同样出现拦截页面,输入我们自定义注册的用户即可访问到预期的页面。
使用自定义签名服务
接着使用上面的数据库表。
1、实现UserDetaisService接口定义签名服务
//这里直接标记为@Service,作为bean扫描进IoC,省去bean的单独配置
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//普通点的dao层接口,获取相应的用户数据和角色权限
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获取数据库用户信息
RoleUser user = userMapper.getUserByName(username);
//获取数据库角色信息
List<Role> roles = userMapper.listRolesByUserName(username);
//将信息转换为UserDetails对象
//权限列表
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
//赋予查询到的角色
for(Role role : roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleName());
authorities.add(authority);
}
//创建UserDetails对象,设置用户名、密码和权限 这里的编码器要保持一致
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDetails userDetails = new User(user.getUserName(), passwordEncoder.encode(user.getUserPwd()), authorities);
return userDetails;
}
}
@Configuration
public class RoleConfig3 extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码编码器-在Spring5的Security中都要求使用密码编码器,否则会发生异常
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
}
2、重新启动Spring Boot项目
可以发现Console中不再为我们打印秘钥了,随即在浏览器中输入任意一个Controller中存在的url,同样出现拦截页面,输入我们自定义注册的用户即可访问到预期的页面。
3、小结
可以发现,实现UserDetailsService这种方式最灵活,用户数据不仅可以来源于数据库,还可以在数据库压力大的情况下转而访问缓存获取用户数据。
限制请求
上面只是实现了验证用户,并且赋予了用户某些角色权限,任何技术如果不是用于解决生产问题,那就是耍流氓。
前面说到,继承WebSecurityConfigurerAdapter 经常使用的三个需要重新方法是
@Configuration
public class RoleConfig extends WebSecurityConfigurerAdapter {
/**
* <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
super.configure(auth);
}
/**
* <p>用来配置Filter链
* */
@Override
public void configure(WebSecurity web) throws Exception {
// TODO Auto-generated method stub
super.configure(web);
}
/**
* <p>用来配置拦截保护请求,比如什么请求放行,什么请求需要验证
* */
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
}
}
前面重写了configure(AuthenticationManagerBuilder auth)实现了用户验证和赋予用户角色权限。接下来,就需要重写configure(HttpSecurity http)实现限制请求,如下。
1、重写configure(HttpSecurity http)方法
@Override
protected void configure(HttpSecurity http) throws Exception {
//限定签名后的权限
http.
/**第一段**/
authorizeRequests()
//限定"/user/welcome"请求赋予ROLE_USER 或者 ROLE_ADMIN
.antMatchers("/user/welcome", "/user/details").hasAnyRole("USER", "ADMIN")
//限定"/admin/**"下所有请求权限赋予ROLE_ADMIN
.antMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN")
//其它路径允许签名后访问
.anyRequest().permitAll()
/**第二段**/
//对于没有配置权限的其它请求允许匿名访问
.and().anonymous()
//使用Spring Security的默认登录页面
.and().formLogin()
//HTTP基础验证
.and().httpBasic();
}
2、现身说法
上面的配置,拥有ADMIN权限的可以访问"/admin/**"路径下的所有,拥有USER权限或ADMIN的可以访问"/user/welcome"和"/user/details",其它路径可以匿名访问。
对于这里的限制请求配置,明显产生了权限冲突,针对此问题,Spring Security采取了配置优先原则来解决,比如上面第二段允许匿名的访问,且没有给出uri地址,但是第一段中加入了限制,还是会采取第一段的限制访问。
因此,生产中,需要把具体的配置防止前面,把不具体的配置放到后面。
还应该注意方法上是否需要加上"ROLE_"的区别。
另外,除了使用上面的Ant风格编码,还可以使用正则规则,例如
http.authorizeRequests()
.regexMatchers("/user/welcome", "/user/details").hasAnyRole("USER", "ADMIN")
.regexMatchers("/admin/.*").hasAnyAuthority("ROLE_ADMIN")
.and().formLogin()
.and().httpBasic();
3、其它权限方法说明
方法 | 说明 |
access(String) | 参数为Spring EL,如果返回true则允许访问,需要配合Spring EL表达式使用 |
anonymous() | 允许匿名访问 |
authorizeRequests() | 限定通过签名的请求 |
anyRequest() | 限定任意的请求 |
hasAnyRole(String ...) | 将访问权限赋予多个角色(角色会自动加入前缀"ROLE_") |
hasRole(String) | 将访问权限赋予一个角色(角色会自动加入前缀"ROLE_") |
permitAll() | 无条件允许访问 |
and() | 连接词,并取消之前限定的前提规则 |
httpBasic() | 启用浏览器的HTTP基础验证 |
formLogin() | 启用Spring Security默认的登录页面 |
not() | 对其它方法的访问采取求反 |
fullyAuthorized() | 如果是完整验证(并发Remember-me),则允许访问 |
denyAll() | 无条件不允许任何访问 |
hasIpAddress(String) | 如果是给定的IP地址则允许访问 |
rememberme() | 用户通过Remember-me功能验证就允许访问 |
hasAnyAuthority(String ...) | 如果是给定的多个角色就允许访问(角色不会自动加入前缀"ROLE_") |
hasAuthority(String) | 如果是给定的一个角色就允许访问(角色不会自动加入前缀"ROLE_") |
4、使用Spring EL表达式配置访问权限(等价于Ant风格配置)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//使用Spring EL 表达式现在只有角色ROLE_USER或者ADMIN
.antMatchers("/user/**").access("hasRole('USER') or hasRole('ADMIN')")
//设置访问权限给角色ADMIN,要求是完整登录(非记住我登录)
.antMatchers("/admin/welcome1").access("hasAuthority('ROLE_ADMIN') && isFullyAuthenticated()")
//设置访问权限给角色ADMIN,允许使用非完整登录(使用记住我登录)
.antMatchers("/admin/welcome2").access("hasAuthority('ROLE_ADMIN')")
//使用记住我功能
.and().rememberMe()
//Spring Security默认登录页面
.and().formLogin()
//http基础验证
.and().httpBasic();
}
上面的代码中,在access()方法里用了3个Spring表达式,除此之外,Spring Security还提供了以下表达式方法:
方法 | 说明 |
authentication() | 用户认证对象 |
denyAll() | 拒绝任何访问 |
hasAnyRole(String ...) | 赋予一个或多个角色权限,会自动加上"ROLE_" |
hasRole(String) | 赋予一个角色权限,会自动加上"ROLE_" |
hasIpAdress(String) | 是否请求来自指定IP |
isAnonymous() | 是否匿名访问 |
isAuthenticated() | 是否用户通过认证签名 |
isFullyAuthenticated() | 限制完整登录(非记住我功能) |
isRememberMe() | 是否通过"记住我"功能通过验证 |
hasAuthority(String) | 赋予一个角色权限,需要手动加上"ROLE_" |
hasAnyAuthority(String ...) | 赋予一个或多个角色权限,需要手动加上"ROLE_" |
permitAll() | 无条件允许任何访问 |
principal() | 用户的principal对象 |
强制使用HTTPS
1、简单的示例
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//使用安全渠道,强制使用https
.requiresChannel().antMatchers("/admin/**").requiresSecure()
.and()
//不使用https
.requiresChannel().antMatchers("/user/**").requiresInsecure()
.and()
//限定角色访问权限
.authorizeRequests().antMatchers("/admin/**").hasAnyRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("ROLE", "ADMIN");
}
2、现身说法
这里的requiresChannel()方法说明使用通道,然后antMatchers()是一个限定请求,然后requiresSecure()表示使用HTTPS请求,而requiresInsecure()则是取消安全请求的限制,这样就可以使用普通的HTTP。
防止跨站点伪造请求
Spring Security在默认的情况下,是已经开启了防止CSRF攻击的过滤器,如果需要关闭(当然是不建议),可以这么做:
http.csrf().disable().authorizeRequests()...
Spring Security会为每次需要提交的表单提供一个key-value形式的参数,这个信息不存在Cookie中,所以无法伪造,然后根据客户端传递的参数和服务端比较,正确才会放行。
用户认证功能
1、自定义登录页面并使用"记住我"功能
简单的示例
http
//设置角色访问权限
.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
.and()
//启用remember me功能
.rememberMe().tokenValiditySeconds(86400).key("remember-me-key")
.and()
//启用HTTP Basic功能
.httpBasic()
.and()
//通过签名后可以访问任何路径
.authorizeRequests().antMatchers("/**").permitAll()
.and()
//设置默认的登录页和跳转路径
.formLogin().loginPage("/login/page")
.defaultSuccessUrl("/admin/welcome1");
现身说法
这里的 rememberMe()方法就是启用"记住我"功能,有效时间为86400s(即1天),在浏览器中以Cookie存储,键"remember-me-key"。loginPage()方法设置默认的登录页,defaultSuccessUrl()方法设置默认的跳转路径。
这里的"/login/page"所映射的路径,可以使用传统的Controller控制层去映射,也可以使用新增映射关系去完成,如下:
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* <p>新增映射关系
* */
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//使得/login/page映射为login.jsp
registry.addViewController("/login/page").setViewName("login");
//使得/logout/page映射为logout_welcome.jsp
registry.addViewController("/logout/page").setViewName("logout_welcome");
//使得/logout映射为logout.jsp
registry.addViewController("/logout").setViewName("logout");
}
}
注意
登录页面的参数名必须是账号:username、密码:password、记住我:remember-me,且记住我为一个checkbox,这样Spring Security才能获取这些参数,且提交方式必须为POST。
2、启用HTTP Basic认证
在前面调用httpBasic()方法就是启动了HTTP Basic功能。还可以调用realmName(String)方法为认证设置模态对话框的标题。
3、登出
默认情况下,Spring Security会提供一个URI--"/logout",用POST请求了这个uri,Spring Security就会推出,且清楚remember me的相关信息。仿造自定义登录,也可以实现自定义推出。