写在前面的话
更多Spring与微服务相关的教程请戳这里 火力全开系列 Spring与微服务教程合集 持续更新
核心概念:
认证授权:Spring Security不仅支持基于URL对Web的请求授权,还支持方法访问授权、对象访问 授权等Spring Security已经集成的认证技术如下:
HTTP BASIC authentication headers:一个基于IETF RFC的标准HTTP Digest authentication headers:一个基于IETF RFC的标准HTTP X.509 client certificate exchange:一个基于IETF RFC的标准LDAP:一种常见的跨平台身份验证方式Form-based authentication:用于简单的用户界面需求OpenID authentication:一种去中心化的身份认证方式Authentication based on pre-established request headers:类似于 Computer Associates SiteMinder,一种用户身份验证及授权的集中式安全基础方案Jasig Central Authentication Service:单点登录方案Transparent authentication context propagation for Remote Method Invocation(RMI)and HttpInvoker:一个Spring远程调用协议Automatic "remember-me" authentication:允许在指定到期时间前自行重新登录系统Anonymous authentication:允许匿名用户使用特定的身份安全访问资源Run-as authentication:允许在一个会话中变换用户身份的机制Java Authentication and Authorization Service:JAAS,Java验证和授权APIJava EE container authentication:允许系统继续使用容器管理这种身份验证方式Kerberos:一种使用对称密钥机制,允许客户端与服务器相互确认身份的认证协议除此之外,Spring Security还引入了一些第三方包,用于支持更多的认证技术,如JOSSO等。
如果所有这些技术都无法满足需求,则Spring Security允许我们编写自己的认证技术
注意:
项目引入spring security之后,虽然不做任何配置,也会有一个Http基本认证(由于版本不同,也可能是表单认证)默认用户名是user,密码会在控制台打印;也可以在application.yml中配置用户名密码
如果不重写configure(HttpSecurity http)方法,则默认会有以下行为:
验证所有请求允许表单验证允许Http基本认证import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); } }
HttpSecurity被设计为链式调用,且实际上对应的是XML中的标签
HttpSecurity提供了很多配置相关的方法,分别对应命名空间配置中的子标签<http>。
例如:
authorizeRequests() 对应 <intercept-url>formLogin() 对应 <formlogin>httpBasic() 对应 <http-basic>csrf() 对应 <csrf>调用这些方法之后,除非使用and()方法结束当前标签,上下文才会回到HttpSecurity,否则链式调用的上下文将自动进入对应标签域。
authorizeRequests()方法实际上返回了一个 URL 拦截注册器,我们可以调用它提供的anyanyRequest()、antMatchers()和regexMatchers()等方法来匹配系统的URL,并为其指定安全策略formLogin()方法和httpBasic()方法都声明了认证方式csrf()方法是Spring Security提供的跨站请求伪造防护功能,当我们继承WebSecurityConfigurerAdapter时会默认开启csrf()方法
准备三个controller,分别是AdminController、UserController、AppController,每个controller里面都提供hello方法
/admin/hello需要ADMIN角色才能访问,/user/hello需要USER角色才能访问,/app/hello不需要登录就能访问,为开放资源
@RestController @RequestMapping("/admin") public class AdminController { @RequestMapping("/hello") public String hello(){ return "hello,admin!"; } } @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/hello") public String hello(){ return "hello,user!"; } } @RestController @RequestMapping("/app") public class AppController { @RequestMapping("/hello") public String hello(){ return "hello,app!"; } }
Spring Security支持各种来源的用户数据,包括内存、数据库、LDAP等。它们被抽象为一个UserDetailsService接口, 任何实现了UserDetailsService 接口的对象都可以作为认证数据源
下面配置了来源于内存的用户,并为用户分配角色,且spring security 5之后密码必须进行加密,否则登录时会报错
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasRole("USER") .antMatchers("/app/**").permitAll() .anyRequest().authenticated() .and().formLogin() .and().csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String pwd = encoder.encode("ok"); auth.inMemoryAuthentication().passwordEncoder(encoder) .withUser("bobo").password(pwd).roles("ADMIN") .and() .withUser("tudou").password(pwd).roles("USER"); } }
除了InMemoryUserDetailsManager,Spring Security还提供另一个UserDetailsService实现类:JdbcUserDetailsManager JdbcUserDetailsManager帮助我们以JDBC的方式对接数据库和Spring Security,它设定了一个默认的数据库模型
默认数据库模型脚本在/org/springframework/security/core/userdetails/jdbc/users.ddl中
JdbcUserDetailsManager需要两个表,其中users表用来存放用户名、密码和是否可用三个信息,authorities表用来存放用户名及其权限的对应关系。
该语句是用hsqldb创建的,而MySQL不支持varchar_ignorecase这种类型,将varchar_ignorecase改为MySQL支持的varchar即可
create table users( username varchar(50) not null primary key, password varchar(500) not null, enabled boolean not null ); create table authorities ( username varchar(50) not null, authority varchar(50) not null, constraint fk_authorities_users foreign key(username) references users(username) );
基于内存和基于jdbc两种方式没太大区别,只是基于jdbc会多一步设置数据源的操作
JdbcUserDetailsManager封装了操作数据库的细节,比如createUser方法实际上对应insert into user
项目启动时,会向数据库创建用户,但再次启动就会报错,因为数据库中已经有这两个用户了,解决办法是创建用户之前先判断该用户是否存在;但使用内存方式就不会出现重启问题,因为每次重启内存中的用户信息会清空
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import javax.sql.DataSource; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasRole("USER") .antMatchers("/app/**").permitAll() .anyRequest().authenticated() .and().formLogin() .and().csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String pwd = encoder.encode("ok"); JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> configurer = auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(encoder); if(!configurer.getUserDetailsService().userExists("bobo")){ configurer.withUser("bobo").password(pwd).roles("ADMIN"); } if(!configurer.getUserDetailsService().userExists("tudou")){ configurer.withUser("tudou").password(pwd).roles("USER"); } } }
自定义数据库结构实际上是实现一个自定义的UserDetailsService
SysUser只是单纯地与数据库交互,不提供身份认证和权限认证能力
public class SysUser { private Integer id; private String username; private String password; //逗号分隔的角色 private String roles; //省略get/set方法 }
SecurityUser类继承SysUser类,且实现UserDetails接口,该接口提供身份认证和权限认证能力
UserDetail接口源码:
package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public interface UserDetails extends Serializable { //获取权限 Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); //下面四个方法均返回true就行 boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
SecurityUser类:
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; public class SecurityUser extends SysUser implements UserDetails { public SecurityUser(){} public SecurityUser(SysUser sysUser){ super.setId(sysUser.getId()); super.setUsername(sysUser.getUsername()); super.setPassword(sysUser.getPassword()); super.setRoles(sysUser.getRoles()); } //spring security用于存放权限的属性 private List<GrantedAuthority> authorityList; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorityList; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public void setAuthorityList(List<GrantedAuthority> authorityList) { this.authorityList = authorityList; } }
ISysUserMapper采用mybatis实现:
import com.bobo.group.springsecuritybase.entity.SysUser; import org.apache.ibatis.annotations.Select; public interface ISysUserMapper { @Select("select * from sys_user where username=#{username}") SysUser getUserByUsername(String username); }
CustomUserDetailService:
import com.bobo.group.springsecuritybase.dao.ISysUserMapper; import com.bobo.group.springsecuritybase.entity.SecurityUser; import com.bobo.group.springsecuritybase.entity.SysUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; @Service public class CustomUserDetailService implements UserDetailsService { @Autowired private ISysUserMapper iSysUserMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { SysUser sysUser = iSysUserMapper.getUserByUsername(s); if(null == sysUser){ throw new UsernameNotFoundException("该用户不存在!"); } //将数据库形式的roles转换为UserDetails识别的权限集 //commaSeparatedStringToAuthorityList方法的实现是按逗号分隔,我们也可以借助SimpleGrantedAuthority类提供其它实现 List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getRoles()); SecurityUser securityUser = new SecurityUser(sysUser); securityUser.setAuthorityList(list); return securityUser; } }