概述
Spring Security是一个安全管理框架,从Spring Boot开始对其提供了自动化配置方案,可以零配置使用Spring Security
引入Spring Security
1. Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入Maven依赖之后就会获得以下特性
- 所有的HTTP请求都会进行身份验证
- 使用HTTP基本的身份验证进行验证
- 只有一个用户为: user,密码在启动时随机生成到控制台
配置Spring Security
Spring Security 提供了几种配置用户存储的选项
- 基于内存的用户存储
- 基于JDBC-based的用户存储
- 基于LDAP-backed的用户存储
- 自定义用户身份验证
只需自定义配置类继承WebSecurityConfigurerAdapter,重写configure方法进行设置,
同时需要注解@Configuration @EnableWebSecurity
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置用户存储选项
}
}
1. 密码加密方式选择
- BCryptPasswordEncoder—Applies bcrypt strong hashing encryption
- NoOpPasswordEncoder—Applies no encoding
- Pbkdf2PasswordEncoder—Applies PBKDF2 encryption
- SCryptPasswordEncoder—Applies scrypt hashing encryption
- StandardPasswordEncoder—Applies SHA-256 hashing encryption
- 自定义加密实现PasswordEncoder即可
2. 基于内存的用户存储
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();//加密方式
/** In-memory user store **/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder) //加密方式
.withUser("Tom") //用户名
.password(passwordEncoder.encode("123")) //密码
.authorities("ROLE_USER") //权限
.and()
.withUser("Jerry")
.password(passwordEncoder.encode("456"))
.authorities("ROLE_USER");
}
}
3. 基于JDBC-based的用户存储
默认使用的用户查询SQL,不指定查询sql时spring会默认使用此sql查询
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled " + "from users " + "where username = ?"; public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority " + "from authorities " + "where username = ?"; public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority " + "from groups g, group_members gm, group_authorities ga " + "where gm.username = ? " + "and g.id = ga.group_id " + "and g.id = gm.group_id";
使用自定义的SQL查询
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean //使用@Bean注解则其他地方调用encoder()方法时会自动从SpringContext中查找对应的方法 public PasswordEncoder encoder() { return new StandardPasswordEncoder("53cr3t"); } /** JDBC-based user store **/ @Autowired private DataSource dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义查询(以下包含最少所需字段) String USER_QUERY = "SELECT USERNAME,PASSWORD,ENABLED FROM TEST_USERS WHERE USERNAME = ?"; String AUTHORITY_QUERY = "SELECT USERNAME,AUTHORITY FROM TEST_AUTHORITIES WHERE USERNAME = ?"; auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery(USER_QUERY) //自定义用户查询 .authoritiesByUsernameQuery(AUTHORITY_QUERY) //自定义用户权限查询 .passwordEncoder(encoder()); //设置加密方式,实际是从SpringContext中注入的bean } }
4. 基于LDAP-backed的用户存储
4.1 Maven依赖
基本
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.ldap</groupId> <artifactId>spring-ldap-core</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-ldap</artifactId> </dependency> <!--嵌入式UnboundID服务器:LDAP数据库--> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> </dependency>
也可采用嵌入式ApacheDS服务器: LDAP数据库
<dependency> <groupId>org.apache.directory.server</groupId> <artifactId>apacheds-core</artifactId> <version>1.5.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.directory.server</groupId> <artifactId>apacheds-server-jndi</artifactId> <version>1.5.5</version> <scope>runtime</scope> </dependency>
4.2 LDAP方式一: 使用内嵌LDAP数据库+java方式
- SecurityConfiguration.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/** LDAP-backed user store **/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
//.userDnPatterns("uid={0},ou=people") //根据uid查找,{0}为占位符
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.contextSource()
.root("dc=tacocloud,dc=com") //增加根节点(users.ldif文件中没有设置根节点)
.ldif("classpath:users.ldif") //加载的ldap数据文件地址
//.url("ldap://localhost:33389/dc=tacocloud,dc=com") //使用远程地址连接方式
.and()
.passwordCompare()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.passwordAttribute("userPassword");
}
}
users.ldif文件
#代码中使用.root("dc=tacocloud,dc=com")增加根节点,此处无需设置根节点 #条目根节点: 组织 dn: ou=groups,dc=tacocloud,dc=com objectclass: top objectclass: organizationalUnit ou: groups #条目根节点: 人员 dn: ou=people,dc=tacocloud,dc=com objectclass: top objectclass: organizationalUnit ou: people #条目people下面的节点 dn: uid=buzz,ou=people,dc=tacocloud,dc=com objectclass: top objectclass: person objectclass: organizationalPerson objectclass: inetOrgPerson cn: Buzz Lightyear sn: Lightyear uid: buzz userPassword: password #条目groups下面的节点 dn: cn=tacocloud,ou=groups,dc=tacocloud,dc=com objectclass: top objectclass: groupOfNames cn: tacocloud member: uid=buzz,ou=people,dc=tacocloud,dc=com
4.3 LDAP方式二: <远程数据库连接类似>使用内嵌LDAP数据库+配置文件方式
SecurityConfiguration.java
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { /** LDAP-backed user store **/ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.ldapAuthentication() .userDnPatterns("uid={0},ou=people") //根据uid查找,{0}为占位符 //.userSearchBase("ou=people") //.userSearchFilter("(uid={0})") .groupSearchBase("ou=groups") .contextSource() //.root("dc=tacocloud,dc=com") //.ldif("classpath:users.ldif") .url("ldap://localhost:33389/dc=tacocloud,dc=com") //远程连接方式连接 .and() .passwordCompare() .passwordEncoder(NoOpPasswordEncoder.getInstance()) .passwordAttribute("userPassword"); } }
application.properties配置文件配置内嵌LDAP数据库
# 数据库启动时加载的数据文件 # 根节点 # 端口 spring.ldap.embedded.ldif=classpath:users.ldif spring.ldap.embedded.base-dn=dc=tacocloud,dc=com spring.ldap.embedded.port=33389
users.ldif文件
# 需要在此额外加入根节点 dn: dc=tacocloud,dc=com objectclass: top objectclass: domain objectclass: extensibleObject dc: tacocloud #条目根节点: 组织 dn: ou=groups,dc=tacocloud,dc=com objectclass: top objectclass: organizationalUnit ou: groups #条目根节点: 人员 dn: ou=people,dc=tacocloud,dc=com objectclass: top objectclass: organizationalUnit ou: people #条目people下面的节点 dn: uid=buzz,ou=people,dc=tacocloud,dc=com objectclass: top objectclass: person objectclass: organizationalPerson objectclass: inetOrgPerson cn: Buzz Lightyear sn: Lightyear uid: buzz userPassword: password #条目groups下面的节点 dn: cn=tacocloud,ou=groups,dc=tacocloud,dc=com objectclass: top objectclass: groupOfNames cn: tacocloud member: uid=buzz,ou=people,dc=tacocloud,dc=com
4.4 LDAP基本语法简单介绍
LDAP是轻量级目录访问协议,LDAP数据库是以树结构存储的,查询效率高,但是修改慢
使用LDAP是实现单点登录的一种方式,可以多个系统使用统一LDAP认证服务
# objectClass
# LDAP中,一个条目必须包含一个objectClass属性,且需要赋予至少一个值。每一个值将用作一条LDAP条目进行数据存储的模板;模板中包含了一个条目必须被赋值的属性和可选的属性。
# objectClass有着严格的等级之分,最顶层是top和alias。例如,organizationalPerson这个objectClass就隶属于person,而person又隶属于top。
# objectClass可分为以下3类:
# 结构型(Structural):如person和organizationUnit;
# 辅助型(Auxiliary):如extensibeObject;
# 抽象型(Abstract):如top,抽象型的objectClass不能直接使用。
# 对象类是属性的集合,LDAP预想了很多人员组织机构中常见的对象,并将其封装成对象类。比如人员(person)含有姓(sn)、名(cn)、电话(telephoneNumber)、密码(userPassword)等属性,单位职工(organizationalPerson)是人员(person)的继承类,除了上述属性之外还含有职务(title)、邮政编码(postalCode)、通信地址(postalAddress)等属性。
# 通过对象类可以方便的定义条目类型。每个条目可以直接继承多个对象类,这样就继承了各种属性。如果2个对象类中有相同的属性,则条目继承后只会保留1个属性。对象类同时也规定了哪些属性是基本信息,必须含有(Must 活Required,必要属性):哪些属性是扩展信息,可以含有(May或Optional,可选属性)。
# 在OpenLDAP的schema中定义了很多objectClass,下面列出部分常用的objectClass的名称。
# ● account
# ● alias
# ● dcobject
# ● domain
# ● ipHost
# ● organization
# ● organizationalRole
# ● organizationalUnit
# ● person
# ● organizationalPerson
# ● inetOrgPerson
# ● residentialPerson
# ● posixAccount
# ● posixGroup
# Attribute
# 属性(Attribute)类似于程序设计中的变量,可以被赋值。在OpenLDAP中声明了许多常用的Attribute(用户也可自己定义Attribute)。
# 每个条目都可以有很多属性(Attribute),比如常见的人都有姓名、地址、电话等属性。每个属性都有名称及对应的值,属性值可以有单个、多个,比如你有多个邮箱。
# 属性不是随便定义的,需要符合一定的规则,而这个规则可以通过schema制定。比如,如果一个entry没有包含在 inetorgperson 这个 schema 中的objectClass: inetOrgPerson,那么就不能为它指定employeeNumber属性,因为employeeNumber是在inetOrgPerson中定义的。
# 常见的Attribute含义如下:
# ● c:国家。
# ● cn:common name,指一个对象的名字。如果指人,需要使用其全名。
# ● dc:domain Component,常用来指一个域名的一部分。
# ● givenName:指一个人的名字,不能用来指姓。
# ● l:指一个地名,如一个城市或者其他地理区域的名字。
# ● mail:电子信箱地址。
# ● o:organizationName,指一个组织的名字。
# ● ou:organizationalUnitName,指一个组织单元的名字。
# ● sn:surname,指一个人的姓。
# ● telephoneNumber:电话号码,应该带有所在的国家的代码。
# ● uid:userid,通常指某个用户的登录名,与Linux系统中用户的uid不同。
# 下面列出部分常用objectClass要求必设的属性。
# ● account:userid。
# ● organization:o。
# ● person:cn和sn。
# ● organizationalPerson:与person相同。
# ● organizationalRole:cn。
# ● organizationUnit:ou。
# ● posixGroup:cn、gidNumber。
# ● posixAccount:cn、gidNumber、homeDirectory、uid、uidNumber。
5. 自定义用户身份验证
在此展示使用Spring JPA方式验证实例
5.1 domain: User.java
//域对象需实现UserDetails
@Data
@Entity
@Table(name = "TEST_USERS")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String username;
private String password;
private Boolean enabled;
//返回授予用户权限集合,权限必须"ROLE_XXX"格式
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
//账号是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//是否被锁
@Override
public boolean isAccountNonLocked() {
return true;
}
//凭证是否过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//用户是否启用
@Override
public boolean isEnabled() {
return enabled;
}
}
5.2 Repository (DataSource配置和数据库驱动包引入参考Spring Jpa)
public interface UserRepository extends CrudRepository<User,Integer> {
User findByUsername(String username);
}
5.3 UserDetailsService
/** 实现UserDetailsService接口 **/
@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//验证用户密码操作
User user = userRepository.findByUsername(s);
if(user == null) throw new UsernameNotFoundException("用户"+s+"不存在");
return user;
}
}
5.4 configuration中配置userDetailsService
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
@Qualifier("userRepositoryUserDetailsService") //指定具体哪一个对象@Service对应名称
@Autowired
private UserDetailsService userDetailsService;
/** Customizing user authentication (使用自定义用户身份验证) **/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
}
配置web请求
并不是所有的请求都需要进行身份验证,比如登录页面,注册页面等,因此可以通过配置实现以下功能
- 满足安全条件的请求,服务器才能接收
- 配置自定义登录页面
- 可以使用户退出登录
- 配置跨站点请求伪造保护
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置web请求
}
}
1. 请求验证
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/home","/design")
.hasRole("USER") // /home 与 /design请求需验证"ROLE_USER"权限
.antMatchers("/","/**").permitAll() //其他所有请求无需验证身份
.and()
.formLogin(); //使用表单校验
//.loginPage("/login") 不指定或者不存在时使用Spring默认提供的/login页面
//.and()
//.httpBasic(); //验证方式httpBasic验证
}
}
2. antMatchers()返回对象API介绍(验证规则配置)
即ExpressionUrlAuthorizationConfigurer.AuthorizedUrl对象API
方法 | 描述 |
---|---|
access(String) | 使用SpEL表达式验证通过时才允许访问 |
anonymous() | 允许匿名用户访问 |
authenticated() | 允许通过身份验证的用户访问 |
denyAll() | 无条件拒绝访问 |
fullyAuthenticated() | 允许通过充分身份验证的用户访问(不含记住密码自动登录用户) |
hasAnyAuthority(String…) | 允许有指定权限种任何一种权限的用户访问 |
hasAnyRole(String…) | 允许有指定角色中任何一个角色的用户访问 |
hasAuthority(String) | 允许指定权限的用户访问 |
hasIpAddress(String) | 允许指定IP地址的用户访问 |
hasRole(String) | 允许指定角色的用户访问 http.authorizeRequests().antMatchers(“/home”).hasRole(“USER”) |
not() | 否则其他方法访问规则,not()后面指定 |
permitAll() | 允许所有访问者 |
rememberMe() | 允许通过remember-me认证的用户访问(即记住密码功能自动登录的用户) |
3. SpEL表达式简介
- 语法
Security expression | 描述 |
---|---|
authentication | 用户身份验证对象 |
denyAll | 永远返回false, http.authorizeRequests().antMatchers(“/home”).access(“denyAll()”) |
hasAnyRole(list of roles) | 用户有任何给定角色则返回true |
hasRole(role) | 用户有给定角色则返回true |
hasIpAddress(IP address) | 请求来自给定的IP地址则返回true |
isAnonymous() | 是匿名用户则返回true |
isAuthenticated() | 用户通过身份验证则返回true |
isFullyAuthenticated() | 完全通过身份验证则返回true(不含remember-me认证) |
isRememberMe() | 使用remember-me认证通过则返回true |
permitAll | 永远为true |
principal | 包含用户主要属性的对象 |
- 实例
@Override
protected void configure(HttpSecurity http) throws Exception {
// "/home"请求验证: 拥有ROLE_USER权限并且日期是周二才能通过
http.authorizeRequests()
.antMatchers("/home")
.access("hasRole('USER') && "+
"T(java.time.LocalDate).now().getDayOfWeek().getValue() == "+
"T(java.time.DayOfWeek).TUESDAY"
)
.and()
.formLogin();
}
4. 自定义登录页面
Controller: LoginController.java
//因为都是最简单的Controller,此种方式更简便 @Configuration public class LoginController implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("security/login"); registry.addViewController("/success").setViewName("security/success"); } }
page
login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <form th:action="@{/loginUrl}" method="post"> <input type="text" name="name" placeholder="Username"/><br/> <input type="password" name="pwd" placeholder="Password"/><br/> <input type="submit" value="登录" /> </form> </body> </html>
success.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>success</title> </head> <body> <h1>Successful!</h1> </body> </html>
security配置: SecurityConfiguration.java
@Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/home") // /home请求需要进行验证 .hasRole("USER") .and() .formLogin() //使用表单校验 .loginPage("/login") //自定义登录页面 .loginProcessingUrl("/loginUrl") //设置登录页面请求的url路径(默认为/login) .usernameParameter("name")//自定义登录页面用户名参数名:默认为username .passwordParameter("pwd") //自定义登录页面密码参数名: 默认为password .defaultSuccessUrl("/success",true);//定义验证成功时跳转url地址,true表示强制跳转 //.and() //.logout() //登出配置: 同上(默认使用 /logout请求) //.logoutSuccessUrl("/"); //登出成功跳转页面 } }
5. 防止跨站点请求伪造(CSRF)
Spring Security默认开启了CSRF保护,通过CSRF token实现的(默认保存在session中),只需要在表单中增加字段_csrf即可
如果页面使用Spring MVC 的jsp标签库或者带有Spring Security方言的Thymeleaf,
如
<form th:action="@{/loginUrl}">
,form
标签中使用了Thymeleaf属性,此时页面无需添加任何操作,提交时Spring 会自动添加_csrf
字段<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <form th:action="@{/loginUrl}" method="post"> <!-- 无需额外添加_csrf字段 --> <input type="text" name="name" placeholder="Username"/><br/> <input type="password" name="pwd" placeholder="Password"/><br/> <input type="submit" value="登录" /> </form> </body> </html>
手动添加
_csrf
字段<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <form action="/loginUrl" method="post"> <!-- form标签没有使用th:action此种Thymeleaf属性时需手动引入 --> <input type="hidden" name="_csrf" th:value="${_csrf.token}"/> <input type="text" name="name" placeholder="Username"/><br/> <input type="password" name="pwd" placeholder="Password"/><br/> <input type="submit" value="登录" /> </form> </body> </html>
前后端分离时,以纯html为例
因为CSRF token默认保存在session中,此时前端无法获取,需要更改Spring Security配置保存到cookie中并设置js可读
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> <script> //读取cookie信息 function getCookie(name) { var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)"); if(arr=document.cookie.match(reg)) return unescape(arr[2]); else return null; } window.onload = function(){ //_csrf字段值设置为cookie中保存的token值 document.querySelector("#csrf").value = getCookie("XSRF-TOKEN") } </script> </head> <body> <form action="/loginUrl" method="post"> <!-- 增加_csrf字段 --> <input type="hidden" name="_csrf" id = "csrf"/><br/> <input type="text" name="name" placeholder="Username"/><br/> <input type="password" name="pwd" placeholder="Password"/><br/> <input type="submit" value="登录" /> </form> </body> </html>
//Spring Security配置 @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf()//CSRF配置 .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); //token保存在cookie中,并且设置js可读 } }
禁用csrf
//Spring Security配置 @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf()//CSRF配置 .disable(); //禁用 } }
6. 获取验证后的用户
6.1. 通过Principal对象
Principal对象封装了验证用户主要属性
//Controller中
@RequestMapping("/success")
public String success(Principal principal){
//通过Principal对象获取到验证用户名,然后再根据用户名从数据库中查找该用户
User user = userRepository.findByUsername(principal.getName());
System.out.println(user);
return "security/success";
}
6.2. 通过Authentication对象
Authentication对象封装了用户身份验证信息
@RequestMapping("/success")
public String success(Authentication authentication){
//通过 authentication 对象获取到验证用户
User user = (User) authentication.getPrincipal();
System.out.println(user);
return "security/success";
}
6.3. 通过@AuthenticationPrincipal注解(推荐)
@RequestMapping("/success")
public String success(@AuthenticationPrincipal User user){
//通过 @AuthenticationPrincipal 注解获取到验证用户
System.out.println(user);
return "security/success";
}
6.4. SecurityContext对象
优点: 不仅可以在Controller中使用, 还可以在Spring application中任何地方使用
@RequestMapping("/success")
public String success(){
//通过SecurityContextHolder获取到SecurityContext对象
//再获取Authentication对象,然后再获取到验证用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
System.out.println(user);
return "security/success";
}