一:基于内存的多用户认证授权:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("admin") //需要admin角色才可以访问
.antMatchers("/app/api/**").permitAll() //不设限访问
.antMatchers("/user/api/**").hasRole("user") //需要user角色才可以访问
.anyRequest().authenticated()
.and().formLogin() //启用简单表单方式登录
.and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于内存配置多个用户
InMemoryUserDetailsManager manager =new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password("123456").roles("user").build());
manager.createUser(User.withUsername("admin").password("123456").roles("admin").build());
auth.userDetailsService(manager).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
1.antMatchers是一个采用ANT模式的URL匹配器。ANT模式使用?匹配惹你单个字符,使用*匹配0或任意数量的字符,使用匹配0或更多的目录。antMatchers("/admin/api/")相当于匹配/admin/api/下的所有API。此处我们指定当其必须为admin角色才可以访问。
2.Spring Security支持各种用户数据来源,包括内存,数据库,LDAP等。它们被抽象为一个UserDetailsService接口,任何实现了UserDetailsService接口的对象都可以作为认证数据源。
3.spring security 5 对 PasswordEncoder 做了相关的重构,原先默认配置的 PlainTextPasswordEncoder(明文密码)被移除了,想要做到明文存储密码,只能使用一个过期的类来过渡
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
4.携带csrf token
spring security官方文档中介绍了三种方式:
(1). 表单提交中,使用jsp的标签可以访问后端服务器参数的方式,在表单提交中,可以轻易地将后端的csrf token,以参数名"_csrf"添加到表单参数中
(2). 对于Ajax提交的JSON请求,是无法在请求参数中添加crsf token。面对这种情况,可以将crsf token添加到请求头之中 ,当然这还是需要jsp标签访问后端服务器参数
(3). 而对于使用html+AngularJS 的同学,angularJs提供了一种机制处理XSRF。当提交XHR requests时,$http 从cookie中读取参数名为XSRF-TOKEN的token,然后将它以X-XSRF-TOKEN为参数名设置到HTTP header。 由于只有域名内的js能够读取相应网站的cookie,服务器可以来自js的请求是在域名内的。而对于服务器端的spring security来说,CookieCsrfTokenRepository默认会将csrf token以名XSRF-TOKEN写入cookie,然后接收请求时从名为X-XSRF-TOKEN的header或者名为_csrf的http请求参数读取csrf token。但是CookieCsrfTokenRepository写如的cookie默认具有cookieHttpOnly属性,前端js是不能操作它的,这就需要在spring security的配置中将 cookieHttpOnly属性设为false:
二:基于默认数据库模型的认证与授权:
除了 InMemoryUserDetailsManager基于内存,Spring Security还提供另一个UserDetailsService实现类JdbcUserDetailsManger。JdbcUserDetailsManger帮助我们以JDBC的方式对接数据库和Spring Security,它设定了一个默认的数据库模型。
<--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<--mysql连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
#application.properties中配置数据库连接参数
spring.datasource.url=jdbc:mysql://localhost:3306/springdemo?serverTimezone=GMT%2B8&autoReconnect=true&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=admin
JdbcUserDetailsManger设定了一个默认的数据库模型,Spring Security将该模型定义在/org/springframework/security/core/userdetails/jdbc/users.ddl内,去数据库创建表结构
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));
create unique index ix_auth_username on authorities (username,authority);
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("admin")
.antMatchers("/app/api/**").permitAll()
.antMatchers("/user/api/**").hasRole("user")
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
if (manager.userExists("user"))
manager.createUser(User.withUsername("user").password("123456").roles("user").build());
if (manager.userExists("amdin"))
manager.createUser(User.withUsername("admin").password("123456").roles("admin").build());
auth.userDetailsService(manager)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
当我们重启服务的时候会报如下的错误:
因为每次重启都会再次创建表,user作为username字段的主键,主键是唯一不重复的,所以会报错。只要加上manager.userExists(“amdin”)这个判断就可了。
三:自定义数据库模型的认证与授权:
我们使用InMemoryUserDetailsManager和JdbcUserDetailsManager 两个UserDetailsService实现类。生效方式也很简单,只需要加入spring的IOC容器,就会被Spring Security自动发现并使用。自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService。UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。UserDetails对象包含了一系列在验证会用到的信息,包含用户名,密码,权限以及其他信息,Spring Security会根据这些信息判定验证是否成功。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); //用户拥有的权限
String getPassword(); //用户名
String getUsername(); //密码
boolean isAccountNonExpired(); //用户账户是否过期
boolean isAccountNonLocked(); //用户账号是否被锁定
boolean isCredentialsNonExpired(); //用户密码是否过期
boolean isEnabled(); //用户是否可用
}
不管数据库结构如何变化,只要能构造一个UserDetails即可。
<--用mybatis依赖替换原来的jdbc依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
1.数据库准备:
表为了方便还是用JdbcUserDetailsManger默认的数据模型的表(请参考二:基于默认数据库模型的认证与授权的表结构)
实体类和mapper:
@Data
public class Authorities{
String username;
String authority;
}
@Data
public class Users {
String username;
String password;
boolean enabled;
}
@Mapper
public interface AuthoritiesMapper {
@Select("SELECT * FROM authorities WHERE username=#{username}")
Authorities findByUserName(@Param("username") String username);
}
@Mapper
public interface UsersMapper {
@Select("SELECT * FROM users WHERE username=#{username}")
Users findByUserName(@Param("username") String username);
}
2.实现UserDetailsService
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Autowired
AuthoritiesMapper authoritiesMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户详情
Users users = usersMapper.findByUserName(username);
if(users == null){
//如果用户查不到,返回null,由provider来抛出异常
return null;
}
//查询用户的权限
Authorities authorities = authoritiesMapper.findByUserName(username);
// 将数据库形式的roles解析为UserDetails的权限集
// AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
//提供的用于将逗号隔开的权限集字符串切割成可用权限对象列表的方法
// 当然也可以自己实现,如用分号来隔开等,参考generateAuthorities
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authorities.getAuthority());
//构建UserDetails 对象
UserDetails userDetails = User.withUsername(users.getUsername()).password(users.getPassword()).authorities(grantedAuthorities).build();
return userDetails;
}
3.Spring Security的配置类
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("admin")
.antMatchers("/app/api/**").permitAll()
.antMatchers("/user/api/**").hasRole("user")
.anyRequest().authenticated()
.and().formLogin()
.and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
为什么数据库中的角色总是要添加“ROLE”前缀,在配置化时却并没有’'ROLE"前缀呢?
查看源码即可找到答案:
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(!role.startsWith("ROLE_"),
() -> "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
return "hasRole('ROLE_" + role + "')";
}
如果不希望匹配这个前缀,那么改为调用hasAuthority方法即可。