Spring Security 入门原理及实战
参考博客: https://www.cnblogs.com/demingblog/p/10874753.html
Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。
特别是在spring boot项目中加入spring security更是十分简单, 这里就简单介绍一下入门案例和使用原理吧!
写一个简单的springboot测试例子:
@Controller public class AppController { @RequestMapping("/hello") @ResponseBody String home() { return "Hello ,spring security!"; } }
在springboot中的pom.xml中引入springsecurity, 加入如下代码:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
然后在配置文件中写入
security.basic.enabled=false
spring security 默认提供了表单登录的功能。我们新建一个类SecurityConfiguration
,并加入一些代码,如下所示:
@Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } }
上面的代码其实就是 一种配置,authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护。 formLogin() 定义当需要用户登录时候,转到的登录页面。
此时,我们并没有写登录页面,但是spring security默认提供了一个登录页面,以及登录控制器。我们这里简单写一个自定义的登录页面:
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'> <h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'> <table> <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr> <tr><td>Password:</td><td><input type='password' name='password'/></td></tr> <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr> <input name="_csrf" type="hidden" value="635780a5-6853-4fcd-ba14-77db85dbd8bd" /> </table> </form></body></html>
我们可以发现,这里有个form 。action="/login"
,这个/login
依然是spring security
提供的。form表单提交了三个数据:
- username 用户名
- password 密码
- _csrf CSRF保护方面的内容,暂时先不展开解释
再配置文件加入如下:
security.user.name=admin
security.user.password=admin
重启项目,访问被保护的/hello页面。自动跳转到了spring security 默认的登录页面,我们输入用户名admin密码admin。点击Login
按钮。会发现登录成功并跳转到了/hello。
除了登录,spring security还提供了rememberMe功能,这里不做过多解释
通常情况下,我们需要实现“特定资源只能由特定角色访问”的功能。假设我们的系统有如下两个角色:
- ADMIN 可以访问所有资源
- USER 只能访问特定资源
现在我们给系统增加“/product” 代表商品信息方面的资源(USER可以访问);增加"/admin"代码管理员方面的资源(USER不能访问)。代码如下:
@Controller @RequestMapping("/product") public class ProductTestController { @RequestMapping("/info") @ResponseBody public String productInfo(){ return " some product info "; } } ------------------------------------------- @Controller @RequestMapping("/admin") public class AdminTestController { @RequestMapping("/home") @ResponseBody public String productInfo(){ return " admin home page "; } }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin1") // 管理员,同事具有 ADMIN,USER权限,可以访问所有资源 .password("admin1") .roles("ADMIN", "USER") .and() .withUser("user1").password("user1") // 普通用户,只能访问 /product/** .roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/product/**").hasRole("USER") .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }
通过上面的配置, 可以简单实现一个springsecurity的小例子. 下面说一下springsecurity的原理:
Spring Security的核心组件
spring security核心组件有:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面分别介绍。
SecurityContext
安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext的接口定义如下:
public interface SecurityContext extends Serializable { /** * Obtains the currently authenticated principal, or an authentication request token. * * @return the <code>Authentication</code> or <code>null</code> if no authentication * information is available */ Authentication getAuthentication(); /** * Changes the currently authenticated principal, or removes the authentication * information. * * @param authentication the new <code>Authentication</code> token, or * <code>null</code> if no further authentication information should be stored */ void setAuthentication(Authentication authentication); }
SecurityContextHolder
SecurityContextHolder看名知义,是一个holder,用来hold住SecurityContext实例的。其作用就是存储当前认证信息。
在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。
在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,
默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。
它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder。
SecurityContextHolder是一个类,他的功能方法都是静态的(static)。 SecurityContextHolder可以设置指定JVM策略(SecurityContext的存储策略),这个策略有三种: 1. MODE_THREADLOCAL:SecurityContext 存储在线程中。 2. MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。 3. MODE_GLOBAL:SecurityContext 在所有线程中都相同。 SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。在spring security应用中,我们通常能看到类似如下的代码: SecurityContextHolder.getContext().setAuthentication(token);
Authentication
authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
接口有4个get方法,分别获取
Authorities
, 填充的是用户角色信息。Credentials
,直译,证书。填充的是密码。Details
,用户信息。Principal
直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。
因此可以推断其实现类有这4个属性。这几个方法作用如下:
getAuthorities
: 获取用户权限,一般情况下获取到的是用户的角色信息。getCredentials
: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。getDetails
: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)getPrincipal
: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。isAuthenticated
: 获取当前 Authentication 是否已认证。setAuthenticated
: 设置当前 Authentication 是否已认证(true or false)。
UserDetails
UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
方法含义如下:
getAuthorites
:获取用户权限,本质上是用户的角色信息。getPassword
: 获取密码。getUserName
: 获取用户名。isAccountNonExpired
: 账户是否过期。isAccountNonLocked
: 账户是否被锁定。isCredentialsNonExpired
: 密码是否过期。isEnabled
: 账户是否可用。
UserDetailsService
提到了UserDetails
就必须得提到UserDetailsService
, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername
,他可以用来获取UserDetails。
通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);
方法。
我们在实现loadUserByUsername
方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails
,
(通常是一个org.springframework.security.core.userdetails.User
,它继承自UserDetails) 并返回。
在实现loadUserByUsername
方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。
这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException
。
AuthenticationManager
AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication
,其定义如下:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
AuthenticationManager 的作用就是校验Authentication
,如果验证失败会抛出AuthenticationException
异常。AuthenticationException
是一个抽象类,
因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException
,LockedException
,BadCredentialsException
等。
BadCredentialsException
可能会比较常见,即密码错误的时候。
总结:
SpringSecurity是个非常不错的框架,是个颗粒级别的验证框架,和springboot融合的非常完美, 值得学习。
https://www.cnblogs.com/demingblog/p/10874753.html 这篇博客写的非常清楚, 把原理分析的比较透彻, 可以接着学习底层的东西多一点。