先说一下SpringSecurity
是干什么的,SpringSecurity
主要作用有2方面:认证、授权。
- 认证:
Authentication
, 用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。 - 授权:
Authorize
,授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问
权限管理涉及到几个概念:
- 主体(用户id、账号、密码、...)
- 资源(资源id、资源名称、访问地址、...)
- 权限(权限id、权限标识、权限名称、资源id、...)
- 角色(角色id、角色名称、...)
业界通常基于RBAC实现授权。
在单体应用中,我觉得理解为基于角色的访问控制(Role-Based Access Control)是比较合适的,用起来比较方便。
而在当前动辄微服务开发的环境下,个人觉得理解为基于资源的访问控制(Resource-Based Access Control)用起来更方便,因为微服务中各个微服务都当做资源来看待了。
整合
SpringBoot整合SpringSecurity还是比较简单的:
- 引入相关jar包
- 配置Security(配置时会稍麻烦,因为需要理解的比较多)
1. 引入Jar包
比较简单,引入web包和security包就行
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.projectlombok
lombok
2. 启动测试
引入jar包后就可以启动了,启动时会生成随机密码:
-
访问项目就会跳转到登陆页面,默认账号:
user
-
SpringSecurity
自带注销地址:/logout
,访问这个地址会弹出注销页面。
3. 自定义配置
以上是SpringSecurity
自带的认证功能,我们使用时需要根据我们自己的需要自定义一些内容(2方面配置:认证配置,授权配置),例如:
- 登陆的账号密码
- 是否允许表单登陆
- 密码加密的情况
- 权限鉴定
- ......
3.1 认证配置
自定义的配置其实都在同一个类中,认证和授权在不同的方法中,配置类继承WebSecurityConfigurerAdapter
类,重写2个方法就行。
注意,就是这个父类,想要配置什么,点进源码去里面找对应的方法
认证的方式有2种:一是账号密码等认证信息写在配置中,二是账号密码等信息从数据库读取。使用时,取其一
3.1.1 认证信息写在配置中
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 1. 认证配置
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 测试用的,写死的账号密码
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder().encode("123456"))
.roles("ADMIN")
.authorities("/test/t1")
.and()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("USER")
.authorities("/test/t2")
;
}
// 设置密码加密方法
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 2. 授权的配置方法,下面再讲,先空着
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
3.1.2 认证信息从数据库拿
这种方式,配置类简单,但是需要一个用户服务类,来返回一个SpringSecurity封装的一个user对象,直接将服务类放到配置文件中就行。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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 org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入服务类
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
/**
* 1. 认证配置
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl);
}
// 设置密码加密方法,必须设置
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 2. 授权的配置方法,下面再讲,先空着
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
看一下这个服务类怎么写的,先准备一个服务类要返回的UserDetails
对象
- 自定义user实体对象
package com.example.demo.security.userdetails;
import java.util.Collection;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 参考{@link org.springframework.security.core.userdetails.User}这个类,
* 这个类是security设置实体类参数值的时候用的,里面很多方法可以参考使用。
* 比如设置roles和设置authorities的过程,在User类的内部类UserBuilder中
*/
@Data
public class MyUserDetail implements UserDetails {
private static final long serialVersionUID = 1L;
private Long userId;
private String username;
private String name;
private String password;
private boolean status;
private Long deptId;
private String email;
private String mobile;
private String sex;
private String avatar;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date lastLoginTime;
// 角色权限:SpringSecurity中角色和权限都是放在这个里面的,使用起来是一样的,区别在于,角色要加前缀 ROLE_
private Collection authorities;
/**
* 参考{@link org.springframework.security.core.userdetails.User.UserBuilder}
* 中的roles方法
* @param roles
* @return
*/
public List roles(String... roles) {
List authorities = new ArrayList<>(roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"),
() -> role + " cannot start with ROLE_ (it is automatically added)");
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return authorities;
}
/**
* 参考{@link org.springframework.security.core.userdetails.User.UserBuilder}
* 中的 authorities 方法
* @param authorities
* @return
*/
public List authorities(String authorities) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
}
public List authorities(String... authorities) {
return AuthorityUtils.createAuthorityList(authorities);
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public boolean isAccountNonExpired() {
// 先按这个判断,需要什么自己添加
return this.isStatus();
}
@Override
public boolean isAccountNonLocked() {
return this.isStatus();
}
@Override
public boolean isCredentialsNonExpired() {
return this.isStatus();
}
@Override
public boolean isEnabled() {
return this.isStatus();
}
}
- 用户服务类
package com.example.demo.security.userdetails;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.example.demo.dao.MyUserDao;
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private MyUserDao myUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUserDetail userDetail = myUserDao.getMyUserDetail(username);
// 注意数据库中保存的密码,要是加密过的,就是在配置类中设置的加密方法
if (!userDetail.isEnabled()) {
throw new DisabledException("账号状态异常!");
} else if (!userDetail.isCredentialsNonExpired()) {
throw new LockedException("密码过期!");
}
// 模拟一点角色权限信息,角色前面要加 ROLE_ 前缀
userDetail.setAuthorities(userDetail.authorities("/test/t1", "/test/t2", "ROLE_ADMIN", "ROLE_ROOT"));
return userDetail;
}
}
使用上面这2种方法之一,我们就可以用我们自己的账号和密码登陆了。
3.1.3 看看SpringSecurity自带的
上面是我们自己实现的接口,写了过程。其实,SpringSecurity自己也封装了很多,我们也可以看看。
官方包里面的就是用户的实现过程,其实我们可以用自带的这些,但是限制比较多,拿jdbc这个来说,他也重写了
loadUsersByUsername()
。
但是它限制了很多东西,表名、字段等要符合人家要求:你要有
users
表,表中要包含这些字段:
如果你要想使用,初始化时传入
DataSource
即可,他会根据你传入的数据源自动查找数据。
3.2 授权配置
注意:前提条件是,认证时,账号信息中加入了角色和权限的一些信息,这里才能进行权限判定。
授权配置常用的有2种方式,一是在SecurityConfig
类中,一是用注解表达式。
3.2.1 在SecurityConfig
类配置权限
授权配置还是在上面的SecurityConfig
类中,只不过是在下面的那个方法中配置。
package com.example.demo.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 org.springframework.security.crypto.password.PasswordEncoder;
import com.example.demo.entity.Result;
import com.example.demo.security.userdetails.UserDetailsServiceImpl;
import cn.hutool.json.JSONUtil;
/**
* SpringSecurity配置
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入服务类
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
/**
* 1. 认证配置
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl);
}
// 设置密码加密方法
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 2. 授权的配置方法,下面再讲,先空着
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 1. 登陆登出设置
http
// 允许表单登陆
.formLogin()
// 自定义登陆页面,注意action提交地址 和 账号密码表单name
// .loginPage("/login.html")
// 自定义后端登陆地址,security默认的是/login
// .loginProcessingUrl("/doLogin")
// 自定义登陆成功后的处理,前后端分离一般返回json数据
.successHandler(new MyAuthenticationSuccessHandler())
// 自定义登陆失败后的处理,前后端分离一般返回json数据
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout()
// 自定义退出地址
// .logoutUrl("/logout")
// 退出成功后的处理
.logoutSuccessHandler((req,res,aut)->{
res.setContentType("application/json;charset=utf-8");
Result result = new Result<>();
result.setStatus(1);
result.setCode("200");
result.setMsg("退出成功");
res.getWriter().write(JSONUtil.toJsonStr(result));
})
//使得session失效,默认true
// .invalidateHttpSession(true)
//清除认证信息,默认true
// .clearAuthentication(true)
//删除指定的cookie
// .deleteCookies("cookie01")
;
// 2. 跨域问题
http.csrf().disable();
// 3. 权限设置
http
// 对url进行访问权限控制
.authorizeRequests()
// 按角色来控制权限的
.antMatchers("/test/t2").hasRole("ADMIN")
.antMatchers(
"/admin1/**",
"/admin2/**"
).hasAnyRole("ADMIN1", "ADMIN2")
// 按Authority,有权限才能访问
.antMatchers("/user/**").hasAuthority("/u/a")
.antMatchers("/test/t1").hasAuthority("/test/t1")
// 直接放行的
.antMatchers("/app/**").permitAll()
// 其他任何请求都需要登陆
.anyRequest().authenticated()
;
}
}
3.2.2 注解表达式配置权限
SpringSecurity
的权限注解有5个,都是用在方法上的,分别是:
-
@Secured
:检查指定的角色权限,角色要加前缀ROLE_
,可以多个,如:@Secured({"ROLE_A", "ROLE_B"})
-
@PreAuthorize
:方法执行前进行权限检查,一般都是用这个。用法:@PreAuthorize("hasRole('admin')")
、@PreAuthorize("hasAuthority('/t1') and hasAuthority('/t2')")
、@PreAuthorize("hasAnyRole('root','admin')")
-
@PostAuthorize
:方法执行后进行权限检查,还没用过 -
@PreFilter
:过滤函数,未用过 -
@PostFilter
:过滤函数,未用过
注意:
- 要想使用注解,需要开启注解,在security的配置类加上
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
- 注释掉配置类方法中,关于权限的配置。
注解表达式使用如下:
package com.example.demo.controller;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/t1")
public String test1(String name, HttpServletResponse response) {
response.addHeader("userId","123");
return name == null?"zhangsan":name;
}
@GetMapping("/t2")
@PreAuthorize("hasAnyRole('ROOT','ADMIN')")
public String test2() {
return "test2";
}
@GetMapping("/t3")
@PreAuthorize("hasAuthority('/test/t3')")
public String test3() {
return "test3";
}
@GetMapping("/t4")
@PreAuthorize("hasAuthority('/t4') and hasAuthority('/t5')")
public String test4() {
return "test4";
}
@GetMapping("/t5")
@PreAuthorize("hasRole('admin')")
public String test5() {
return "test5";
}
}
注解的判断方法走的是类org.springframework.security.access.expression.SecurityExpressionRoot
中的,可以看看逻辑。