Spring Boot实践 | 利用Spring Security快速搞定权限控制
目录
开始之前
快速开始
使用内存签名服务
使用数据库签名服务
使用自定义签名服务
限制请求
强制使用HTTPS
防止跨站点伪造请求
用户认证功能
在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机制,还可以给与用户赋予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// TODO Auto-generated method stubsuper.configure(auth);}/*** <p>用来配置Filter链* */@Overridepublic void configure(WebSecurity web) throws Exception {// TODO Auto-generated method stubsuper.configure(web);}/*** <p>用来配置拦截保护请求,比如什么请求放行,什么请求需要验证* */@Overrideprotected void configure(HttpSecurity http) throws Exception {// TODO Auto-generated method stubsuper.configure(http);}
}
2、定义签名服务
根据上面所述,我们只需要重写下面的方法即可。
/*** <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// TODO Auto-generated method stubsuper.configure(auth);}
完整的示例
@Configuration
public class RoleConfig extends WebSecurityConfigurerAdapter {/*** <p>用来配置用户签名服务,主要是user-detail机制,还可以给与用户赋予角色* */@Overrideprotected 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 {//注入数据源@Autowiredprivate 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机制,还可以给与用户赋予角色* */@Overrideprotected 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层接口,获取相应的用户数据和角色权限@Autowiredprivate UserMapper userMapper;@Overridepublic 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 {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected 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机制,还可以给与用户赋予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// TODO Auto-generated method stubsuper.configure(auth);}/*** <p>用来配置Filter链* */@Overridepublic void configure(WebSecurity web) throws Exception {// TODO Auto-generated method stubsuper.configure(web);}/*** <p>用来配置拦截保护请求,比如什么请求放行,什么请求需要验证* */@Overrideprotected void configure(HttpSecurity http) throws Exception {// TODO Auto-generated method stubsuper.configure(http);}
}
前面重写了configure(AuthenticationManagerBuilder auth)实现了用户验证和赋予用户角色权限。接下来,就需要重写configure(HttpSecurity http)实现限制请求,如下。
1、重写configure(HttpSecurity http)方法
@Overrideprotected 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风格配置)
@Overrideprotected 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、简单的示例
@Overrideprotected 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>新增映射关系* */@Overridepublic void addViewControllers(ViewControllerRegistry registry) {//使得/login/page映射为login.jspregistry.addViewController("/login/page").setViewName("login");//使得/logout/page映射为logout_welcome.jspregistry.addViewController("/logout/page").setViewName("logout_welcome");//使得/logout映射为logout.jspregistry.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的相关信息。仿造自定义登录,也可以实现自定义推出。
Spring Boot实践 | 利用Spring Security快速搞定权限控制相关推荐
- Spring Boot + Vue + Shiro 实现前后端分离、权限控制
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://sina.lt/gauR 本文总结自实习中对项 ...
- Spring Boot + SpringSecurity + JWT 实现简单的 restful Api 权限控制
对于前后端分离的项目,后端对于接口访问的权限控制是必须要做的,也就是需要根据用户的权限进行控制,这样才能对我们的接口资源进行一定程度的保护,在一个web项目中,我们的通常做法是,允许登录后的用户进行接 ...
- Spring Boot Web应用集成Keycloak进阶之细粒度权限控制
[](()资源管理 ======================================================================= 资源管理主要用来定义资源服务器的哪些 ...
- Spring Boot + Vue + Shiro 实现前后端分离、权限控制 (附源码)
点击上方[全栈开发者社区]→右上角[...]→[设为星标⭐] 原项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Free ...
- Spring Boot 实践折腾记(11):使用 Spring 5的WebFlux快速构建效响应式REST API
关于Spring 5中的反应式编程支持Reactor类库,上一篇文章< Spring Boot 实践折腾记(10):2.0+版本中的反应式编程支持--Reactor>已经简要介绍过,Spr ...
- Spring Boot实践——Spring AOP实现之动态代理
Spring AOP 介绍 AOP的介绍可以查看 Spring Boot实践--AOP实现 与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改 ...
- Spring boot中使用Spring Security的记住我 remember-me功能
Spring boot中使用Spring Security的记住我 remember-me功能 问题描述:Spring security新手,在登录时加上记住我功能,需要使用框架自带的记住我. 记住的 ...
- Spring Boot实践
Spring Boot实践 在本文中,我将重点介绍Spring Boot特有的实践(大多数时候,也适用于Spring项目).以下依次列出了最佳实践,排名不分先后. 1.使用自定义BOM来维护第三方依赖 ...
- Spring Boot 实践折腾记(12):支持数据缓存Cache
不管是什么类型的应用程序,都离不开数据,即便如现在的手机APP,我们依然需要使用数数据库,对于不懂的人,当然,我们可以告诉他们一些高大上的概念,但是作为专业人士,就一定要明白背后的真实原理到底是什么. ...
最新文章
- VMware Server 2.0简单学习!
- eclipse下classes文件夹无法发布到tomcat的问题--tomcat发布慢的问题
- LoadRunner解决超时错误
- 门限的限意思是什么_门限是什么意思_门限英文翻译
- 值得收藏的Python小技巧:这17个骚操作你都OK吗?
- stackexchange_通过Spring Social推特StackExchange –第1部分
- Redis五种数据类型及应用场景
- vs各个版本的编译器号
- MacBook Pro M1 安装 VMware Fusion 及 CentOS 8
- vscode安装使用教程
- 好玩的猜数游戏(不是二分查找!四位数)
- 小红书种草笔记怎么写?种草笔记标题怎么写比较好
- 信创办公--基于WPS的Word最佳实践系列(使用智能图形丰富表达内容)
- Python处理视频文件的实用姿势
- javaweb项目实战(附有源码)
- android tools ignore,android tools属性引用
- 匹兹堡大学申请条件计算机科学,匹兹堡大学工程学院计算机科学专业申请条件...
- LANL Earthquake Prediction收获
- css父div半透明子div不透明
- BIM建模师考试试题及答案