Spring Boot实践 | 利用Spring Security快速实现权限控制

本文详细介绍了如何在Spring Boot应用中利用Spring Security进行权限控制。从快速入门到使用内存、数据库和自定义签名服务,再到限制请求、强制HTTPS、防止CSRF攻击和实现用户认证功能,全面展示了Spring Security在实际项目中的应用。通过自定义登录页面和启用HTTP Basic认证,实现安全的用户登录和退出流程。
摘要由CSDN通过智能技术生成

目录

开始之前

快速开始

使用内存签名服务

使用数据库签名服务

使用自定义签名服务

限制请求

强制使用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里面看到随机生成的秘钥,如下。

20190611155946525.png

随即在浏览器中任意输入一个存在于Spring Boot项目中的url,就会出现一个拦截页面,输入刚刚生成的秘钥即可访问到预期的页面,如下。

20190611160244493.png20190611160344186.png

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、创建数如下据表,并配置好数据源等其他配置

20190611170125299.png

20190611170140157.png20190611170150784.png

说明: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的相关信息。仿造自定义登录,也可以实现自定义推出。

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值