4.4.2 会话控制
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
机制 | 描述 |
always | 如果没有session存在就创建一个 |
ifRequired |
如果需要就仓U建一个Session (默认)登录时 |
never | SpringSecurity将不会创建Session ,但是如果应用中其他地方创建了Session ,那么Spring Security将会使用它。 |
stateless | SpringSecurity将绝对不会创建Session ,也不使用Session |
通过以下配置方式对该选项进行配置:
@Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) }
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session ,就是ifRequired。
若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了 session ,月B么Spring Security会用它的。
若使用stateless,则说明Spring Security对登录成功的用户不会创建Session ,你的应用程序也不会允许新建 session。并且它会暗示不使用cookie ,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及袞状态认证机制。
会话超时
可以在sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s ; spring boot配置文件:
server.servlet.session.timeout=3600s
session超时之后,可以通过Spring Security设置跳转的路径。
http.sessionManagement() .expiredUrl( "/login-view?error=EXPIRED_SESSI0Nn") .invalidSessionUrl("/login-view?error=INVALID_SESSION");
expired指session过期,invalidSession指传入的sessionid无效。
安全会话cookie
我们可以使用httpOnly和secure标签来保护我们的会话cookie :
- httpOnly :如果为true ,那么浏览器脚本将无法访问cookie
- secure :如果为true,则cookie将仅通过HTTPS连接发送
spring boot配置文件:
server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true
4.6 退出
Spring security默认实现了logout退出,访问/logout,果然不出所料,退出功能Spring也替我们做好了。
点击"Log Out”退出成功。
退出后访问其它url判断是否成功退出。
这里也可以自定义退出成功的页面:
在WebSecurityConfig的protected void configure(HttpSecurity http)中配置:
.and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login-view?logout");
当退出操作出发时,将发生:
- 使HTTP Session 无效
- 清除 SecurityContextHolder 路径转到 /login-view?logout
但是,类似于配置登录功能,咱们可以进一步自定义退出功能:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() //... .and() .logout() (1) .logoutUrl("/logout") (2) .logoutSuccessUrl("/login-viewPlogout") (3) .logoutSuccessHandler(logoutSuccessHandler) (4) .addLogoutHandler(logoutHandler) (5) .invalidateHttpSession(true); (6) }
(1 )提供系统退出支持,使用WebSecurityConfigurerAdapter会自动被应用。
(2 )设置触发退出操作的URL (默认是/logout )。
(3 )退出之后跳转的URL,默认是/login?logout。
(4 )定制的LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl()的设置会被忽略。
(5 )添加一个LogoutHandler ,用于实现用户退出时的清理工作.默认SecuritycontextLogoutHandler会被添加为—个 LogoutHandler。
(6 )指定是否在退出时让HttpSession无效。默认设置为true。 注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF ,必须使用
post方式请求/logout
logoutHandler :
一般来说,LogoutHandler的实现类被用来执行必要的清理,因而他们不应该抛出异常。 下面是Spring Security提供的一些实现:
- PersistentTokenBasedRememberMeServices 基于持久化token的RememberMe功能的相关清理
- TokenBasedRememberMeService 基于token的RememberMe功能的相关清理
- CookieClearingLogoutHandler 退出时Cookie的相关清理
- CsrfLogoutHandler 负责在退出时移除csrfToken
- SecurityContextLogoutHandler 退出时Securitycontext的相关清理 链式API提供了调用相应的LogoutHandler实现的快捷方式,比如deleteCookies().
4.7 授权
4.7.1 概述
授权的方式包括web授权和方法授权,web授权是通过url拦截进行授权,方法授权是通过方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecuritylnterceptor;若为方法授权则拦截器为Methodsecurityinterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方法授权。最后决策通过,则允许访问资源,否则将禁止访问。
4.7.2 准备环境
4.7.2.1 数据库环境
在t_user数据库创建如下表:
角色表:
CREATE TABLE 't_role' ( 'id' varchar(32) NOT NULL, 'role_name' varchar(255) DEFAULT NULL, 'description' varchar(255) DEFAULT NULL, 'create_time' datetime DEFAULT NULL, 'update_time' datetime DEFAULT NULL, 'status' char(l) NOT NULL, PRIMARY KEY ('id'), UNIQUE KEY 'unique_role_name' ('role_name') )ENGINE=InnoDB DEFAULT CHARSET=utf8 insert into 't_role'('id','role_name', 'description','create_time', ' update_time', 'status') values ('1','管理员',NULL,NULL,NULL)
用户角色关系表:
CREATE TABLE 't_user_role' ( 'userid' varchar(32) NOT NULL, 'roleid' varchar(32) NOT NULL, 'create_time' datetime DEFAULT NULL, 'creator' varchar(255) DEFAULT NULL, PRIMARY KEY ('userid','role_id') )ENGINE=InnoDB DEFAULT CHARSET=utf8 insert into 't_user_role'('user_id','roleid','create_time','creator') values ('1','1',NULL,NULL);
权限表:
CREATE TABLE 't_permission' ( 'id' varchar(32) NOT NULL, 'code' varchar(32) NOT NULL COMMENT,权限标识符 'description' varchar(64) DEFAULT NULL COMMENT '描述 'url' varchar(128) DEFAULT NULL COMMENT '请求地址 PRIMARY KEY ('id') )ENGINE=InnoDB DEFAULT CHARSET=utf8 insert into 't_permission' (' id',' code','description',' url') values (' 1’,’ pl','测试资源 l','/r/rl'),('2','p3','测试资源2','/r/r2');
角色权限关系表:
CREATE TABLE 't_role_permission' ( 'roleid' varchar(32) NOT NULL, 'permission_id' varchar(32) NOT NULL, PRIMARY KEY ('role_id','permissionid') )ENGINE=InnoDB DEFAULT CHARSET=utf8 insert into 't_role_permission'(' role_id','permission_id') values ('1','1')
4.7.2.2 修改 UserDetailService
1、修改dao接口
在UserDao中添加:
〃根据用户id查询用户权限 public List<String> findPermissionsByUserId(String userld){ String sql=...略 List<PermissionDto> list = jdbcTemplate.query(sql,new Object[]{userid}new BeanPropertyRowMappero(PermissionDto.class)); List<String> permissions = new ArrayList<>(); list.iterator().forEachRemaining(c->permissions.add(c.getCode())); return permissions;
2、修改UserDetailService
实现从数据库读取权限
@0verride public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //登录略 System.out.printin("username="+username); //根据账号去数据库查询... UserDto user = userDao.getUserByUsername(username); if(user == null)( return null; } //查询用户权限 List<String> permissions = userDao.findPermissionsByUserId(user.getId()); Stringf] perarray = new String[permissions.size()]; permissions.toArray(perarray); //创建 userDetails UserDetails userDetails = User.withUsername(user.getFullname()).password(user.getPassword()).authorities(perarray)・ build(); return userDetails; }
4.7.2 web授权
在上面例子中我们完成了认证拦截,并对/r/**下的某些资源进行简单的授权保护,但是我们想进行灵活的授权控制该怎么做呢?通过给http.authorizeRequests()添加多个子节点来定制需求 ,如下代码:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() (1) .antMatchers("/r/rl").hasAuthority("pl") (2) .antMatchers("/r/r2").hasAuthority("p2") (3) .antMatchers("/r/rS").access("hasAuthority('p1') and hasAuthority('p2')") (4) .antMatchers("/r/**")]authenticated() (5) .anyRequest().permitAll() ⑹ .and() .formLogin() }
(1 ) http.authorizeRequests()方法有多个子节点,每个macher按照他们的声明顺扇丸行。
(2)指定"/r/r1 “URL,拥有p1权限能够访问
(3 )指定”/r/r2"URL ,拥有p2权限能够访问
(4 )指定了”/r/r3”URL ,同时拥有p1和p2权限才能够访问
(5 )指定了除了r1、r2、r3之外’7r/**’'资源,同时通过身份认证就能够访问,这里使用SpEL ( Spring Expression Language )表达式。
(6 )剩余的尚未匹配的资源,不做保护。
注意: 规则的顺序是重要的,更具体的规则应该先写.现在以/admin开始的所有内容都需要具有ADMIN角色的身份验证用 户,即使是/
admin / login路径(因为/ admin / login已经被/ admin / **规则匹配,因此第二个规则被忽略).
.antMatchers("/admin/*\*").hasRole("ADMIN") .antMatchers("/admin/login").permitAll()
因此登录页面的规则应该在/ admin / **规则之前.例如.
.antMatchers("/admin/login").permitAll() .antMatchers("/admin/*\*").hasRole("ADMIN")
保护URL常用的方法有:
authenticated()保护URL ,需要用户登录 permitAII()指定URL无需保护,一般应用与静态资源文件
hasRole(String role)限制单个角色访问,角色将被增加"ROLE_".所以’ADMIN"将和"ROLE_ADMIN"进行比较.
hasAuthority(String authority)限制单个权限访问
hasAnyRole(String… roles)允许多个角色访问. hasAnyAuthority(String… authorities)允许多个权限访问.
access(String attribute)该方法使用SpEL表达式,所以可以创建复杂的限制.
haslpAddress(String ipaddressExpression)限制IP地址或子网
4.7.3 方法授权
现在我们已经掌握了使用如何使用http.authorizeRequests()对web资源进行授权保护,从Spring Security2.0版本开始,它支持服务层方法的安全性的支持。
@PreAuthorize,@PostAuthorize, @Secured三类注解。
我们可以在任(可@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注解的安全性。
以下内容将启用Spring Security的@Secured注释。
@EnableGlobalMethodSecurity(securedEnabled = true) public class MethodSecurityConfig (// ... }
然后向方法(在类或接口上)添加注解就会限制对该方法的访问。Spring Security的原生注释支持为该方法定义了 一组属性。这些将被传递给AccessDecisionManager以供它作出实际的决定:
public interface BankService { @Secured() public Account readAccount(Long id); @Secured() public Accountf] findAccounts(); @Secured() public Account post(Account account, double amount); }
以上配置标明readAccount、findAccounts方法可匿名访问,底层使用WebExpressionVoter投票器,可从 AffirmativeBased 第23 行代码跟踪。 post方法需要有TELLER角色才能访问,底层使用RoleVoter投票器。
使用如下代码可启用prePost注解的支持
@EnableGlobalMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig { //... }
相应Java代码如下:
public interface BankService { @PreAuthorize("isAnonymous()") public Account readAccount(Long id); @PreAuthorize("isAnonymous()") public Accountf] findAccounts。; @PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')") public Account post(Account account, double amount); }