Spring Security
spring系列的安全框架
与shiro比较优缺点
- 缺点:
- 配置非常繁琐,没有shiro简单
- 优点:
- 扩展性与自制性非常好
- 作为Spring家族成员,与spring boot /cloud整合非常简单
- 更够解决spring cloud的安全问题
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
笔记版本 SpringSecurity5.6.1 SpringBoot 2.6.2
Ⅰ 环境搭建
创建一个spring boot项目,导入security依赖,那么所有的项目接口就会受到security的保护
Ⅱ 基础知识点
介绍一些非代码性的知识点
1. 如何实现请求拦截
原生的请求拦截就是添加Filter ,security就是在原生的Filter链中加入了FilterChainProxy使得我们自定义的Filter会被执行
2. 过滤器链
Ⅲ SpringSecurity的默认认证配置
SpringSecurity是如何实现,只加入相关依赖就可以实现系统接口的所有认证但方式是默认的
1. 默认流程图
2. 默认配置
SpringBootWebSecurityConfiguration
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 开启认证 所有请求 默认认证 表单认证
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
return http.build();
}
默认装配生效有两个前提条件
// 有这两个类 则开启默认配置
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
// 没有这两个类 则开启默认配置
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class Beans {
}
得出结论:
如果不想开启默认认证那就继承WebSecurityConfigurerAdapter或者SecurityFilterChain
3. 默认数据源装配
由SpringBoot提供的默认装配类实现
UserDetailsServiceAutoConfiguration
@Bean
@Lazy // 内存user数据源管理器 是 UserDetailsService 的实现类
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser(); // 从配置文件加载类中获取User数据
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager( // 通过properties.getUser()获取的用户数据构建数据源 用户名、密码、角色
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
默认装配数据源生效条件
没有找到一下4个类型的类
@ConditionalOnMissingBean(value =
{ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class
})
**得出结论:**若想自定义数据源,则可以实现其中任意一个接口
4. 默认认证流程
流程
-
UsernamePasswordAuthenticationFilter存在于SecurityFilterChain中,只要开启默认认证,认证请求就要经过这个类。UsernamePasswordAuthenticationFilter会在方法attemptAuthentication中完成认证
-
attemptAuthentication并不具备认证功能所以会调用ProviderManager完成认证
- 在ProvicerManager处Security采用全局Manager+多子Manager策略。也就是每一个ProvicerManager自身都会存储一个全局ProvicerManager,属性名为parent。当ProvicerManager自身无法完成认证时,会调用parent的认证方法完成认证。实际完成认证的不是ProviderManager,而是旗下所有的AuthenticationProvider
-
ProviderManager会调用所有的AuthenticationProvider进行认证,有一个不通过则认证失败
-
默认流程中ProviderManager旗下只有一个AuthenticationProvider即DaoAuthenticationProvider
-
DaoAuthenticationProvider自身不具备数据源,它会调用UserDetailsService(接口)的实现类InMemoryUserDetailsManager获取数据源
- DaoAuthenticationProvider认证成功后,一步步返回到UsernamePasswordAuthenticationFilter的调用者处,进行下一步
-
InMemoryUserDetailsManager中的user信息由UserDetailsServiceAutoConfiguration自动装配得来
-
UserDetailsServiceAutoConfiguration装配数据源从SecurityProperties中获取
5. 默认认证中涉及的重要成员
① AuthenticationManager、ProviderManager、AuthenticationProvider
- AuthenticationManager 是认证管理器的顶级接口
- ProviderManager是AuthenticationManager的一个实现
- ProviderManager管理着多个AuthenticationProvider,在认证时会遍历AuthenticationProvider,有一个不通过则return false
ProviderManager与ProviderManager的关系
在ProvicerManager处,Security采用全局Manager+多子Manager策略。也就是每一个ProvicerManager自身都会存储一个全局ProvicerManager,属性名为parent。当ProvicerManager自身无法完成认证时,会调用parent的认证方法完成认证。实际完成认证的不是ProviderManager,而是旗下所有的AuthenticationProvider
② WebSecurityConfigurerAdapter
如果我们写一个WebSecurityConfigurerAdapter的继承类,则Spring不会开启Security的默认认证
若想自定义Spring Security认证配置就需要写WebSecurityConfigurerAdapter的继承类
③ UserDetailsService
数据源的顶级接口,我们可以通过实现UserDetailsService来自定义数据源,
一但存在UserDetailsService的实现类,自动SpringBoot的自动装配也会失效,不再使用默认数据源
Ⅳ 自定义认证流程(未分离)
流程
- 自定义拦截配置
- 自定义登录页面以及路径
- 自定义认证成功/失败相应
- 自定义注销路径
- 自定义数据源
- 自定义验证码
1. 自定义拦截
在第Ⅲ部分得出,若要关闭SpringSecurity默认配置则需要继承WebSecurityConfigurerAdapter, SecurityFilterChain
以继承WebSecurityConfigurerAdapter为例
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/index/** ").permitAll() // 拦截路径为/index/**的请求,并放行
.anyRequest().authenticated() // 拦截所有请求,并认证
.and()
.formLogin(); // 认证方式为表单认证
}
}
这样就可以实现基本的拦截
2. 自定义登录
在上述过程中自动跳转的登陆界面为Spring Security默认登录页
接下来自定以登录界面
步骤
- 编写登陆界面
- 根据登录界面内容设置,拦截认证行为
① 编写登陆界面
通过第Ⅲ部分源码分析,SpringSecurity默认情况下登录页面必须满足4个条件
- 请求方式 method = “POST”
- 请求路径 /login
- 账号 name=“username”
- 密码 name=“password”
这四个值,都可以自定义
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{/doLogin}" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit"/>
</form>
</body>
</html>
② 修改拦截配置
步骤
-
编写登陆页面跳转接口
-
放行/login.html
-
设置自定义登录页
-
设置登录页请求路径(自定义登录页完成)
// 登陆页面跳转接口
@RequestMapping("/login.html")
public String login(){
return "login";
}
http.authorizeRequests()
.mvcMatchers("/index/**").permitAll()
.mvcMatchers("/login.html").permitAll() // 放行登录页访问路径
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html") // 设置自定义登录页访问路径 必须与loginProcessingUrl一起使用
.loginProcessingUrl("/doLogin") // 设置需要捕获的登录请求(也就是登录页的登录请求) 必须与loginPage一起使用
.and()
.csrf().disable() // 关闭远程请求保护
3. 自定义响应
响应有失败与成功,以及前后端分离/未分离
① 未分离
成功响应
- successForwardUrl() 成功后显示的资源,不管你之前访问的什么资源,统一转发到该资源
- defaultSuccessUrl() 成功后默认显示的资源,若之前有请求资源,则重定向到请求资源
二者不可同时使用
http.authorizeRequests()
.mvcMatchers("/index/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
//.successForwardUrl("/hello") // 设置登录成功后一定转发的路径
.defaultSuccessUrl("/hello") // 设置登录成功后默认重定向的路径,若访问其他资源则不跳转默认路径
.and()
.csrf().disable()
失败响应
- failureForwardUrl() 失败后转发到该资源,错误异常存放在request中
- failureUrl() 失败后重定向到该资源,错误资源存放在session中
错误信息以 key-value 存储 key:“SPRING_SECURITY_LAST_EXCEPTION”
http.authorizeRequests()
.mvcMatchers("/index/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.failureForwardUrl("/error.html")
.failureUrl("/error.html")
.and()
.csrf().disable();
<h1 th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></h1>
<h1 th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></h1>
② 分离
成功响应
successHandler() 参数为AuthenticationSuccessHandler接口的实现类
Spring 默认帮我们实现了几个,当然也可以自定义
successForwardUrl -> ForwardAuthenticationSuccessHandler
defaultSuccessUrl -> SavedRequestAwareAuthenticationSuccessHandler
步骤
- 自定义响应类
- 修改拦截响应
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
}
}
http.authorizeRequests()
.mvcMatchers("/index/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
// 不同之处
.successHandler(new MyAuthenticationSuccessHandler())
.and()
.csrf().disable() // 关闭远程请求保护
失败响应
failureHandler()参数为AuthenticationFailureHandler实现类
步骤
- 自定义响应类
- 修改拦截响应
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> result = new HashMap<>();
result.put("code",200);
result.put("success",true);
result.put("authenticationInfo",authentication);
String valueAsString = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(valueAsString);
}
}
http.authorizeRequests()
.mvcMatchers("/index/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
// 不同之处
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.csrf().disable() // 关闭远程请求保护
4. 自定义注销
默认实现
也就是Spring Security的默认实现
默认实现为发送 GET 方式的 /logout 请求,注销成功后自动跳转到登陆页面
在我们的拦截配置中加入
.and()
.logout() // 开启自定义注销配置
.logoutUrl("/logout") // 注销请求路径,默认是GET方式
.invalidateHttpSession(true) // 清除session
.clearAuthentication(true) // 清除认证信息
自定义注销
自定义则可以自己定义注销页面、路径、请求方式、注销成功后跳转的界面
.and()
.logout() // 开启自定义注销配置
.logoutRequestMatcher(new OrRequestMatcher( // 指定匹配器 或类型,满足其中一个注销请求即可注销
new AntPathRequestMatcher("/logout_get","GET"), // get 注销
new AntPathRequestMatcher("/logout_post","POST") // post 注销
))
.invalidateHttpSession(true) // 清除session
.clearAuthentication(true) // 清除认证信息
.logoutSuccessUrl("/login.html") // 设置注销后跳转的页面
5. 自定义数据源
在默认情况下,我们只能修改和使用SpringSecurity提供的默认数据源进行验证登录,为了实现真正的业务,必须自定义数据源
SpringSecurity提供了两种方式
① 修改默认的ProviderManager
这里SpringSecurity并没有直接为我们提供ProviderManager,而是给了构造器
@Autowired
public void initialize(AuthenticationManagerBuilder managerBuilder) throws Exception {
// 在这里可以修改很多默认的ProviderManager配置
// 修改数据源 这里模拟一下,创建一个InMemoryUserDetailsManager
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
managerBuilder.userDetailsService(userDetailsManager);
}
如果只修改数据源,我们可以简化一下代码
当Spring发现有自定义UserDetailsService时,会帮我自动注入
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
return userDetailsManager;
}
② 自定义ProviderManager
自定义ProviderManager会完全覆盖系统默认的ProviderManager。即便你在容器中加入了UserDetailsService实现类,也需要自己手动设置
实现
重写WebSecurityConfigurerAdapter中的configure(AuthenticationManagerBuilder auth)方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
auth.userDetailsService(userDetailsManager);
}
③ 自定义UserDetailsService
在自定义ProviderManger的基础上,自定义UserDetailsService,以及UserDetail
步骤:
- 自定义UserDetail,编写用户数据源实体类User
- 自定义UserDetailsService,注入UserMapper,并实现数据获取
- 自定义ProviderManager,设置自定义的UserDetailsService
// 自定义UserDetail,编写用户数据源实体类User
/**
* 实现UserDetail 添加必要字段,实现必要方法
*/
public class User implements UserDetails {
// 主键
private Integer id;
// 密码 必要字段
private String password;
// 账号 必要字段
private String username;
// 账号是否过期 必要字段
private boolean accountNonExpired;
// 账号是否锁定 必要字段
private boolean accountNonLocked;
// 密码是否过期 必要字段
private boolean credentialsNonExpired;
// 是否启用 必要字段
private boolean enabled;
// 角色
private Set<Role> roles;
/**
* 获取授权信息
* 我们一般使用的是角色,需要转换一下
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : this.roles) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}
// 自定义UserDetailsService,注入UserMapper,并实现数据获取
@Component
public class MyUserDetailsServer implements UserDetailsService {
// 注入属性
@Autowired
private UserMapper userMapper;
/**
* 实现用户信息加载方法
* 设置数据源
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUserName(username);
if (ObjectUtil.isNull(user))
throw new UsernameNotFoundException("账号未找到");
List<Role> roles = userMapper.getRoles(user.getId());
user.setRoles(new HashSet<>(roles));
return user;
}
}
// 自定义ProviderManager,设置自定义的UserDetailsService
@Autowired
private MyUserDetailsServer userDetailsServer;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServer);
}
6. 自定义验证码
这个过程需要自定义过滤器,流程有点类似前后端分离
流程
- 引入验证码生成依赖
- 编写验证码配置类
- 编写验证码获取接口
- 自定义认证过滤器
- 将自定义过滤器加入过滤器链
- 设置过滤器必要属性
① 引入验证码生成依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
② 编写验证码配置类
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class KaptchaConfig {
@Bean
public Producer producer() {
Properties properties = new Properties();
// 宽度
properties.setProperty("kaptcha.image.width", "150");
// 高
properties.setProperty("kaptcha.image.height", "50");
// 那些字符组成
properties.setProperty("kaptcha.textproducer.char.string", "asdfghjkl");
// 长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
③ 编写验证码获取接口
@GetMapping("/verCode")
public void getVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取验证码文本
String verCode = producer.createText();
// 将验证码存入session,方便认证时获取 后期可放入redis中
request.getSession().setAttribute("verCode",verCode);
// 将文本生成图片
BufferedImage producerImage = producer.createImage(verCode);
// 设置相应格式
response.setContentType(MediaType.IMAGE_PNG_VALUE);
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(producerImage,"png",outputStream);
}
④ 自定义认证过滤器
自定义过滤器,我们只需要在官方认证过滤器之前加上验证码认证即可
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final String VERIFICATION_CODE = "verCode";
private String verificationParameter = VERIFICATION_CODE;
public String getVerificationParameter() {
return verificationParameter;
}
public void setVerificationParameter(String verificationParameter) {
this.verificationParameter = verificationParameter;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String verificationReq = request.getParameter(getVerificationParameter());
String verCode = (String) request.getSession().getAttribute("verCode");
if (!(StrUtil.isNotBlank(verificationReq)&&StrUtil.isNotBlank(verCode)&&verificationReq.equalsIgnoreCase(verCode))){
throw new UsernameNotFoundException("验证码不正确");
}
return super.attemptAuthentication(request,response);
}
}
⑤ 将自定义过滤器加入过滤器链
在configure中加入
http.addFilterAt(filter(), UsernamePasswordAuthenticationFilter.class);
暴露自定义AuthenticationManager
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
设置过滤器属性
@Bean
public MyUsernamePasswordAuthenticationFilter filter() throws Exception {
MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setFilterProcessesUrl("/doLogin");
filter.setUsernameParameter("name");
filter.setPasswordParameter("password");
filter.setAuthenticationSuccessHandler(((request, response, authentication) -> response.sendRedirect("/index")));
filter.setAuthenticationFailureHandler(((request, response, authentication) -> response.sendRedirect("/error")));
return filter;
}
注:必须在这里设置,configure的配置要整理掉
⑥ 设置过滤器必要属性
Ⅴ 自定义认证流程(分离)
认证流程中,分离与部分只有获取前端传递的认证参数的地方不同
- 未分离 从request的请求体中获取username,password
- 分离 从 request的请求体的Json中获取username,password
分析之后我们得出只要扩展 UsernamePasswordAuthenticaitonFilter 中参数获取方式即可
步骤
- 自定义拦截配置类
- 自定义扩展认证过滤器
- 将自定义过滤器加入过滤器链
- 自定义验证码校验
1. 自定义拦截配置类
实现内容与不分离不一样,需要添加一个没有认证时访问资源的异常处理
- 不分离,在此情况下向客户返回登录页
- 分离,在此情况下向前端返回需要认证json
@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
// 给前端相应一个 访问资源需要认证的异常处理
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
System.out.println(authException.getMessage());
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
response.getWriter().print(authException.getMessage());
})
.and()
.csrf().disable();
}
}
2. 自定义扩展认证过滤器
继承UsernamePasswordAuthenticaitonFilter并扩展参数获取流程
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 判断是否是JSON格式,如果是进行处理 ,不是交给父类处理
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
Map<String, String> loginInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// 动态获取数据 ,方便后期修改维护,不写死
String username = loginInfo.get(getUsernameParameter());
String password = loginInfo.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authenticationToken);
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
e.printStackTrace();
}
}
return super.attemptAuthentication(request, response);
}
}
3. 将自定义过滤器加入过滤器链
在将自定义的过滤器加入链之前,还需要一些必要操作
- 自定义数据源,自定义认证管理器
- 将过滤器加入容器中
- 完善过滤器
- 将过滤器加入过滤器链
① 自定义数据源,自定义认证管理器
自定义AuthenticationManager并且向外暴露,同时设置自定义数据源
// 自定义AuthenticationManager 并设置自定义数据源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin").build());
auth.userDetailsService(userDetailsManager);
}
// 向外暴露AuthenticationManager
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
② 将自定义过滤器加入容器中
@Bean
public MyUsernamePasswordAuthenticationFilter myFilter() throws Exception {
MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
return filter;
}
③ 完善过滤器
- 配置认证请求
- 配置认证成功、失败响应
- 配置注销响应
- …
// 设置登录路径
filter.setFilterProcessesUrl("/doLogin");
// 设置认证成功结果
filter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
HashMap<String, Object> result = new HashMap<>();
result.put("code",200);
result.put("success",true);
result.put("authenticationInfo",authentication);
String valueAsString = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(valueAsString);
}));
// 设置认证失败响应
filte.setAuthenticationFaildHandler(((request,response,authentication)->{
HashMap<String, Object> result = new HashMap<>();
result.put("code",500);
result.put("success",false);
result.put("authenticationInfo",authentication);
String valueAsString = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(valueAsString);
}));
// 设置注销
filter.setLogoutSuccessHandler((request,response,authentication)->{
HashMap<String, Object> result = new HashMap<>();
result.put("success",true);
String valueAsString = new ObjectMapper().writeValueAsString(result);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(valueAsString);
})
// 将自定义AuthenticationManager设置到自定义过滤器中
filter.setAuthenticationManager(authenticationManagerBean());
响应相关handler结构
④ 将自定义过滤器加入过滤器链
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
http.addFilterAt(myFilter(),UsernamePasswordAuthenticationFilter.class);
}
4. 自定义验证码校验
步骤
- 添加验证码生成依赖
- 编写验证码配置类
- 编写验证码获取接口
- 开放验证码接口
- 修改过滤器,添加验证码校验
① 添加验证码生成依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
② 编写验证码配置类
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class KaptchaConfig {
@Bean
public Producer producer() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "asdfghjkl");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
③ 编写验证码获取接口
@GetMapping("/verCode")
public String getVerCode(HttpServletRequest request) throws IOException {
String verCode = producer.createText();
request.getSession().setAttribute("verCode",verCode);
BufferedImage image = producer.createImage(verCode);
FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
ImageIO.write(image,"png",outputStream);
return Base64.encodeBase64String(outputStream.toByteArray());
}
④ 开放验证码接口
.mvcMatchers("/verCode").permitAll()
⑤ 修改过滤器,添加验证码校验
添加一下代码,部分省略
private final String VERIFICATION_CODE = "verCode";
private String verificationParameter = VERIFICATION_CODE;
public String getVerificationParameter() {
return verificationParameter;
}
public void setVerificationParameter(String verificationParameter) {
this.verificationParameter = verificationParameter;
}
// 在重写的attemptAuthentication的用户密码封装之前添加一下代码
String verCode = userInfo.get(getVerificationParameter());
String verCodeReq = (String) request.getSession().getAttribute(getVerificationParameter());
if (!(StrUtil.isNotBlank(verCode)&&StrUtil.isNotBlank(verCode)&&verCode.equalsIgnoreCase(verCodeReq))){
throw new UsernameNotFoundException("验证码错误");
}
Ⅵ 获取认证数据
通过代码方式获取一些SpringSecurity封装的数据,比如,认证信息、授权信息
1. 获取认证信息
重点
-
我们可以通过SecurityContextHolder中的SecurityContextHolderStrategy成员属性获取用户数据
-
SecurityContextHolderStrategy有四种策略,常用三种
- ThreadLocalSecurityContextHolderStrategy将用户数据保存在ThreadLocal中,在子线程中无法获取用户信息
- InheritableThreadLocalSecurityContextHolderStrategy将用户数据保存在ThreadLocal中,在子线程中无法获取用户信息
- GlobalSecurityContextHolderStrategy将用户数据存放在ApplicationContext中,太耗资源,不推荐
-
SecurityContextHolderStrategy的默认策略是ThreadLocalSecurityContextHolderStrategy
-
通过源码发现,修改SecurityContextHolder的strategyName属性值可以达到修改策略的目的
-
private static String strategyName = System.getProperty("spring.security.strategy");
-
我们需要修改系统属性,需要再jvm启动时修改参数,不能通过配置文件实现
-
使用代码获取用户认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 可以强转为我们使用的数据源对象
Ⅶ 密码加密/校验/更新
Security提供给我们很多加密方式,就是PasswordEncoder的实现类,可以直接使用
Spring Security的密码校验设计的非常巧妙
- 如果容器中有指定加密方式,那么Security会直接使用指定加密方式进行校验**(不推荐)**
- 如果没有,则会采用非常灵活的策略机制,从数据库密码中获取前缀,根据前缀匹配密码解析器
1. 密码加密
Security提供给我们很多加密方式,就是PasswordEncoder的实现类,可以直接使用
我们以BCrypt加密为例
采用更推荐的加密方式,拼接加密前缀
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10); // 参数为散列10次
String password = passwordEncoder.encode("123");
String encryptionPassword = "{bcrypt}"+password; // 拼接加密前缀 其他加密前缀可以翻源码
System.out.println(encryptionPassword);
2. 密码校验一
直接指定项目加密方式,(不推荐,不灵活)
特点
- 密码加密后不需要在密码中拼接加密前缀,也不能添加
- 无法兼容之前数据库老密码
实现
在容器中添加对应的密码校验器就行
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
3. 密码校验二
采用策略模式,根据密码前缀匹配密码校验器(Spring Security默认方式,推荐),Security自动对密码进行加密验证
特点
- 可以兼容老密码
- 加密方式可以多种多样
- 需要在加密的密码前拼接加密前缀
Spring Security默认加密方式为 new BCryptPasswordEncoder(10),也就是散列次数为10
4. 密码自动更新升级
在认证成功后,security会判断容器中是否存在密码更新Bean(UserDetailsPasswordService),存在则会进行调用
- 实现UserDetailsPasswordService,实现updatePassword方法
- 参数newPassword,就是Security采用默认加密方式更新的密码(Security认为安全的)
- 不能与密码校验一 一起使用
@Component
public class CustomUserDetailService implements UserDetailsService, UserDetailsPasswordService {
private final UserMapper userMapper;
@Autowired
public CustomUserDetailService(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUserName(username);
if (ObjectUtil.isNull(user))
throw new UsernameNotFoundException("账号不存在");
List<Role> roles = userMapper.getRoles(user.getId());
user.setRoles(new HashSet<>(roles));
return user;
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
// 调用数据库更新密码
System.out.println(newPassword);
// 设置新密码
((User) user).setPassword(newPassword);
// 返回user,不然会报空指针
return user;
}
}
Ⅷ 记住我
1. 开启记住我
在我们的拦截配置中添加**rememberMe()**即可
http.authorizeRequests()
...
.and()
.rememberMe()
...
添加此方法后,过滤器链中的RememberMeAuthenticationFilter将会启用,拦截所有请求
若要实现记住我功能,请求需要携带参数 remember-me,值可以为 true/1/on/yes
2. 记住我实现流程
流程分为两部分生成登录令牌、自动登录
自动登录根据登录令牌实现自动登录
① 生成登录令牌
在系统开启自动登录,SecurityContextHolder中没有认证信息、cookie中没有登录令牌时会进入该流程(也就是第一次登录)
- RememberMeAuthenticationFilter在各种条件都不满足的情况下放行
- AbstractAuthenticationProcessingFilter拦截,并调用UsernamePasswordAuthenticationFilter的认证方法进行认证
- 认证成功后AbstractAuthenticationProcessingFilter会调用认证成功的通知方法successfulAuthentication(…)
- successfulAuthentication()方法会调用**AbstractRememberMeServices的loginSuccess()**进行记住我操作流程
- loginSuccess()首先判断是否开启记住我流程,如果开启则会调用子类实现TokenBasedRememberMeServices的onLoginSuccess()完成最终操作
- onLoginSuccess()中
- 根据用户名、密码、签名到期时间以及Security提供的Key(一个UUID)生成一个字符串,再将字符串进行MD5加密生成用户签名
- 再将用户名、签名到期时间、用户签名 经过Base64加密生成登陆令牌,并放入cookie中
以上就是登录令牌的生成流程
② 自动登录
该流程的核心是对登录令牌的检验
- RememberMeAuthenticationFilter拦截请求,如果认证信息过期则会进行自动登录操作
- RememberMeAuthenticationFilter调用**AbstractRememberMeServices的autoLogin()**进行自动登录
- autoLogin()
- 先判断cookie是否含有登录令牌
- 再使用Base64对令牌解码,得到用户名、签名到期时间、签名
- 最后调用子类实现**TokenBasedRememberMeServices的processAutoLoginCookie()**进行签名校验
- processAutoLoginCookie()
- 先判断是否过期
- 再根据用户名获取密码
- 最后使用 用户名、密码、签名到期时间 再使用MD5加密获取新的登录令牌,与cookie中的令牌进行比对
- 自动登陆成功RememberMeAuthenticationFilter会更新认证信息、登录令牌
3. 安全性提高
现有问题
默认情况下的登录令牌非常不安全,无论你登录多少次,令牌都不会变化,一旦被劫取,就完了
如何升级
将令牌生成与校验的实现类TokenBasedRememberMeServices 修改为 PersistentTokenBasedRememberMeServices 即可
区别就是PersistentTokenBasedRememberMeServices每次访问后都会更新令牌,并且令牌不再携带用户名
如何实现
创建PersistentTokenBasedRememberMeServices实体类到容器中
@Bean
public PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices(){
return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsServer,new InMemoryTokenRepositoryImpl());
}
参数:key 用户信息 令牌存放位置(security提供了内存存储)
4. 前后端不分离实现
在后端开启记住我,在前端页面添加check
<input type="checkbox" value="1"> 1/yes/on/true
5. 前后端分离实现
分析源码得
-
AbstractRememberMeServices中的loginSuccess方法会去判断是否开启remember
-
判断方式为从Request请求体中获取remember-me参数,判断值是否为true/on/1/yes
-
由上可得,我们只需要将其获取remember-me参数的地方,改为json获取即可完成前后端分离记住我
-
考虑到我们在认证时已经解析Request的输入流,所以我们可以在那时将remember-me参数解析,并存入request域中
-
在我们修改的remember-me判断处,将其取出
实现步骤
- 在自定义的UsernamePasswordAuthenticationFilter中将参数remember-me存入request域中
- 继承AbstractRememberMeServices的子类,并重写判断方法rememberMeRequested(remember-me)
- 将自定义的AbstractRememberMeServices设置给认证拦截器
- 将自定义的AbstractRememberMeServices设置给记住我拦截器
① 获取remember-me
在自定义的UsernamePasswordAuthenticationFilter中将参数remember-me存入request域中
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
...
String rememberValue = loginInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
// 防止后续空指针异常
if (StrUtil.isNotBlank(rememberValue)){
request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER,rememberValue);
}else {
request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER,"");
}
...
}
② 自定义AbstractRememberMeServices
选择继承实现类PersistentTokenBasedRememberMeServices
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.servlet.http.HttpServletRequest;
public class MyRememberMeServices extends PersistentTokenBasedRememberMeServices {
public MyRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
// 只修改这一句
String paramValue = request.getAttribute(parameter).toString();
if (paramValue == null || !paramValue.equalsIgnoreCase("true") && !paramValue.equalsIgnoreCase("on") && !paramValue.equalsIgnoreCase("yes") && !paramValue.equals("1")) {
this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
return false;
} else {
return true;
}
}
}
③ 将自定义的AbstractRememberMeServices设置给认证拦截器
这是最关键的一步,让拦截器使用我们定义的类
@Bean
public MyRememberMeServices myRememberMeServices(){
return new MyRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());
}
// 在自定义的认证过滤器中设置
@Bean
public MyUsernamePasswordAuthenticationFilter myFilter() throws Exception {
...
filter.setRememberMeServices(myRememberMeServices());
...
}
④ 将自定义的AbstractRememberMeServices设置给记住我拦截器
因为我们继承的并不是Spring Security提供的默认rememberMeService,所以在自动登录的地方也要设置一下
@Override
protected void configure(HttpSecurity http) throws Exception {
...
.rememberMe()
.rememberMeServices(myRememberMeServices())
...
}
Ⅸ 会话管理
会话也就是session管理
本节涉及:
- session并发管理
- session共享
1. 并发处理
session并发,也就是允许一个账号在几个平台同时登录
① 开启并发管理
在这里我们以session并发2为例
省略security前戏
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.sessionManagement()
.maximumSessions(1); // 设置session最大并发量
}
② 设置session并发响应
当session并发超过,我们需要通知用户
前后端未分离
添加配置项
...
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/expired.html") // 设置要跳转的页面
...
前后端分离
实现与前后端分离认证失败等类似
...
.sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("code",500);
hashMap.put("message","其他平台已登陆");
String string = new ObjectMapper().writeValueAsString(hashMap);
response.getWriter().println(string);
response.flushBuffer();
});
...
③ 禁止登录设置
当session超过最大并发量,则禁止客户端登录
在配置项中添加是否可登录
...
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
...
2. 会话共享
当我们应用是集群的时候,以上操作的session并发将会失效。因为多服务器之间的session并不共享,所以需要借助外物,做到会话共享共享
方案是将数据存储到Redis中
实现步骤
- 导入相关依赖
- 配置redis配置
- 在配置项中设置session注册地址
- 测试
① 导入相关依赖
redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis-session相关依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
② 配置redis配置
spring:
redis:
username: root
url: redis://@192.168.37.100:6379
③在配置项中设置注册地址
第一步创建session-redis注册器
private final FindByIndexNameSessionRepository repository;
@Autowired
public SecurityConfig(FindByIndexNameSessionRepository repository) {
this.repository = repository;
}
第二步创建security-session注册器
@Bean
public SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry(){
return new SpringSessionBackedSessionRegistry(repository);
}
第三步在配置项中配置注册器
...
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(springSessionBackedSessionRegistry())
...
④ 测试
将同一个程序启动多个服务
- 复制当前服务
- 在 VM Options设置参数
-Dserver.port=8081
- 启动双服务在多平台测试
Ⅹ CSRF/CORS
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
1. Security防御CSRF
采取在提交是添加_csrf
参数
<input type="hidden" name="_csrf" value="3cc8e049-9026-44c5-8a8c-339e75aad15b">
通过令牌方式防御
2. 防御CSRF
① 不分离
传统项目很简单,直接在认证配置项中开启防御CSRF即可。
开启之后Spring Security会自动在登录页中添加 _csrf
输入框
...
.csrf()
...
分离
3. CORS跨域问题解决
① SpringMVC解决方法
注解解决
直接在接口上添加跨域注解
@CrossOrigin
MVC全局解决
实现mvc全局配置接口
配置跨域相关信息
@Configuration
public class MyMVCConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 对所有请求生效
.allowCredentials(false) // 不需要凭证
.allowedHeaders("*") // 任何头都可以
.allowedMethods("*") // 任何方法
.allowedOrigins("*") // 任何域
.maxAge(3600);
}
}
4. Security解决
因为Security过滤器的优先级高于MVC,所以当我们开启Security的跨域配置后,MVC的跨域配置将不再生效
一般使用MVC的就行
security跨域配置实现
// 添加配置项,开启跨域配置
...
.and()
.cors()
.configurationSource(corsConfigurationSource())
...
// 设置跨域参数
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",configuration);
return urlBasedCorsConfigurationSource;
}
ⅩⅠ 异常处理
Security中的异常一共有两大类
- AuthenticationException 认证异常
- AccessDeniedException 授权异常
每一种的子实现都很多,比如:用户锁定一场、凭证过期异常等等…
1. 默认实现
在Security中,
- 认证异常默认跳转到登陆页面
- 授权默认抛出异常
2. 自定义异常
直接在配置项中添加配置即可
...
.and()
.exceptionHandling() // 开启异常处理
.authenticationEntryPoint(((request, response, authException) -> { // 认证异常处理
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(authException.getMessage());
}))
.accessDeniedHandler(((request, response, accessDeniedException) -> { // 授权异常处理
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().println(accessDeniedException.getMessage());
}))
...
ⅩⅡ 授权管理
这一节盘点安全框架的另一大核心,授权
1. 前提知识
说一些不是授权知识但是授权基础,比如说:什么是授权、什么是权限、授权需要信息在哪里等
① 什么授权
概念
在项目中通常会为资源添加一些权限。也就是说虽然用户进行了认证,但是你可能没有资格访问某些资源,比如说VIP用户于普通用户。所以需要一些授权进行判断。
一般情况下对资源授权分类两种 角色、权限
- 如果项目是角色+资源的方式,那么一个角色通常对应很多资源,需要验证用户是否拥有某角色
- 如果项目是权限+资源方式,那么就需要验证用户是否拥有该权限
- 如果项目是角色+权限+资源方式,那么就是一个角色对应多个权限,一个权限对应多个资源
举例
- 角色+权限+资源
- 一个添加权限可以调用所有添加接口,一个经理角色拥有添加权限、修改权限
② 授权需要的信息在哪里
Security在认证之后会有一个认证对象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;
}
③ Security的授权策略
security提供了两种授权策略**(httpUrl,method)** url授权和方法授权
具体实现
- 基于过滤器的权限管理(FilterSecurityInterceptor)实现对httpUrl的授权
- 拦截http请求,进行授权
- 基于AOP的权限管理(MethodSecurityInterceptor)实现对method的授权
- 对method添加aop操作实现拦截
注:在Security授权中,角色授权会自动加上ROLE_
前缀,资源授权需要手动加上READ_
2. URL授权
① 简单授权配置
直接在配置项中添加请求拦截情况
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/admin").hasRole("admin") // 表示请求该url需要拥有角色 ROLE_admin
.mvcMatchers("/index").hasAuthority("READ_index")
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
表示请求 /admin 需要拥有角色 ROLE_admin
表示请求 /index 需要拥有权限 READ_index
② 路径匹配方法
- antMatchers() 最基础url匹配方法 ,
/admin
只能匹配/admin
的请求 - mvcMatchers() 是antMatchers的升级版,
/admin
可以匹配/admin.html
,可以使用通配符 - regexMatchers() 可以使用正则表达式
路径匹配方法的后续方法
- hasRole(role) 需要有指定的角色
- hasAnyRole(role…) 有任一角色即可
- hasAuthority(权限)需要有指定权限
- hasAnyAuthority(权限…)有任一权限即可
- permitAll()什么都不需要
3. Method授权
方法授权更多的是在方法上加注解,通过注解来配置具体授权
MethodSecurityInterceptor其实是一个切面,方法授权就是给方法加了aop,在之前或者之后做一些事
常用注解
- PreAuthorize()
- PostAuthorize()
- PreFilter()
- PostFilter()
以上注解都支持 权限表达式,能写的内容非常多
① 开启注解支持
在Spring Boot启动类上添加开启直接
@EnableGlobalMethodSecurity(prePostEnabled = true)
② PreAuthorize
前置认证方法,在请求进入方法之前进行一些授权判断
// 需要有admin角色
@PreAuthorize("hasRole('admin')")
// 需要有admin角色,并且登录的用户名为 llz
@PreAuthorize("hasRole('admin') and authentication.name == 'llz'")
// 有任意一个角色即可
@PreAuthorize("hasAnyRole('admin','manager')")
// 登录的用户名需要与参数name一致
@PreAuthorize("authentication.name == #name")
③ PostAuthorize
后置授权注解
④ PreFilter
前置过滤注解
⑤ PostFilter
后置过滤注解
4. 授权原理分析
- 请求被FilterSecurityInterceptor过滤器拦截,doFilter方法调用父类AbstractSecurityInterceptor的beforeInvocation(url)方法完成授权,并返回授权token。
- beforeInvocation方法
- 通过参数中的url从FilterInvocationSecurityMetadataSource的getAttributes(obj)方法中获取该url上的授权条件
- 调用自身attemptAuthorization方法进行授权认证。
- attemptAuthorization中调用自身的AccessDecisionManager的decide方法进行授权。
- decide方法会遍历与该授权相关的AccessDecisionVoter一次进行授权判断
分析源码得出,如果我们想要把数据库中的授权配置信息放入Security(动态配置授权),那么就需要实现FilterInvocationSecurityMetadataSource接口,并且重写getAttributes(obj)方法
5. 动态设置授权配置信息
也就是说路径的授权权限不再写死,而是从数据库中获取
分析源码可得,我们需要实现FilterInvocationSecurityMetadataSource接口重写getAttributes(obj)
实现步骤
-
编写Menu以及Role实体类
-
编写Menu的Mapper
-
自定义授权资源源
-
将自定义的授权资源源交给Security
-
配置用户信息
① 编写Menu以及Role实体类
简单起见并没有设置复杂的Menu以及Role
roles为该资源路径通过授权需要的角色
@Data
public class Menu {
private Integer id;
private String url;
private List<Role> roles;
}
@Data
public class Role {
private Integer id;
private String name;
}
② 编写Menu的Mapper
就是查出所有的Menu对象,并且包含自己授权通过所需的Role
<mapper namespace="com.example.security05authorisation.mapper.MenuMapper">
<resultMap id="menu" type="com.example.security05authorisation.entity.Menu">
<id property="id" column="id"/>
<result property="url" column="url"/>
<collection property="roles" ofType="com.example.security05authorisation.entity.Role">
<id property="id" column="rid"/>
<result property="name" column="name"/>
</collection>
</resultMap>
<select id="getMenuAll" resultMap="menu">
SELECT m.id,m.url,r.id rid,r.`name`
FROM `menu` m
LEFT JOIN role_menu rm ON m.id=rm.menu_id
LEFT JOIN role r ON r.id=rm.role_id
</select>
</mapper>
③ 自定义授权资源源
实现FilterInvocationSecurityMetaSource接口
这个方法最关键的就是获取到该资源路径授权通过所需要的Role列表,不管角色菜单设计的如何,最后要的就是这个东西,将角色列表交给Security创建出Collection即可
package com.example.security05authorisation.config;
import com.example.security05authorisation.entity.Menu;
import com.example.security05authorisation.entity.Role;
import com.example.security05authorisation.mapper.MenuMapper;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
@Component
public class CustomSecurityMetaSource implements FilterInvocationSecurityMetadataSource {
// 路径比较器,比如“/admin/**”与“/admin/a”也是能通过的
AntPathMatcher matcher = new AntPathMatcher();
// 菜单的menuMapper 用来获取菜单资源
private final MenuMapper mapper;
public CustomSecurityMetaSource(MenuMapper mapper) {
this.mapper = mapper;
}
/**
* 实现方法,主要就是该方法了
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取请求url
String requestUrl = ((FilterInvocation)object).getRequestUrl();
// 获取所有的菜单以及菜单所需角色
List<Menu> menuList = mapper.getMenuAll();
for (Menu menu : menuList) {
// 找到对应的菜单
if(matcher.match(menu.getUrl(),requestUrl)){
// 最关键的一步将该资源路径需要所有角色名称放在一个数组中
// 将菜单角色列表转为数组,注意`ROLE_`前缀的坑,用户信息那边不加,这边也不加
String[] roles = menu.getRoles().stream()
.map(p->"ROLE_"+p.getName()).toArray(String[]::new);
// 交由SecurityConfig(Spring)创建Collection<ConfigAttribute>
return SecurityConfig.createList(roles);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
④ 将自定义的授权资源源交给Security
这一步在public class SecurityConfig extends WebSecurityConfigurerAdapter{}
中完成
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomSecurityMetaSource metaSource;
public SecurityConfig(CustomSecurityMetaSource metaSource) {
this.metaSource = metaSource;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 将自定义的授权信息对象交给Security
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(context))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 交给Security
object.setSecurityMetadataSource(metaSource);
// 是否拒绝没有配置授权信息的url的请求,改为false
object.setRejectPublicInvocations(false);
return object;
}
});
// 如果配置授权源,那就不用在这里配置拦截情况了
http.formLogin().and().csrf().disable();
}
}
⑤ 配置用户信息
为了省事就是用了内存用户信息
// 为了省事使用内存用户信息
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin","class").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lzz").password("{noop}123").roles("manager").build());
inMemoryUserDetailsManager.createUser(User.withUsername("zzz").password("{noop}123").roles("class").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
ⅩⅢ OAuth2.0
1. 简介
OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他 们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。
协议特点:
- 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;
- 安全:没有涉及到用户密钥等信息,更安全更灵活;
- 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth
应用场景
- 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、 请求后台数据。
- 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证
- 第三方应用授权登录,比如QQ,微博,微信的授权登录。
优缺点
优点:
- 更安全,客户端不接触用户密码,服务器端更易集中保护
- 广泛传播并被持续采用
- 短寿命和封装的token
- 资源服务器和授权服务器解耦
- 集中式授权,简化客户端
- HTTP/JSON友好,易于请求和传递token
- 考虑多种客户端架构场景
- 客户可以具有不同的信任级别
缺点:
- 协议框架太宽泛,造成各种实现的兼容性和互操作性差
- 不是一个认证协议,本身并不能告诉你任何用户信息
Security http) throws Exception {
// 将自定义的授权信息对象交给Security
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(context))
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O object) {
// 交给Security
object.setSecurityMetadataSource(metaSource);
// 是否拒绝没有配置授权信息的url的请求,改为false
object.setRejectPublicInvocations(false);
return object;
}
});
// 如果配置授权源,那就不用在这里配置拦截情况了
http.formLogin().and().csrf().disable();
}
}
##### ⑤ 配置用户信息
为了省事就是用了内存用户信息
```java
// 为了省事使用内存用户信息
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("llz").password("{noop}123").roles("admin","class").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lzz").password("{noop}123").roles("manager").build());
inMemoryUserDetailsManager.createUser(User.withUsername("zzz").password("{noop}123").roles("class").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
ⅩⅢ OAuth2.0
1. 简介
OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他 们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。
协议特点:
- 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;
- 安全:没有涉及到用户密钥等信息,更安全更灵活;
- 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth
应用场景
- 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、 请求后台数据。
- 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证
- 第三方应用授权登录,比如QQ,微博,微信的授权登录。
优缺点
优点:
- 更安全,客户端不接触用户密码,服务器端更易集中保护
- 广泛传播并被持续采用
- 短寿命和封装的token
- 资源服务器和授权服务器解耦
- 集中式授权,简化客户端
- HTTP/JSON友好,易于请求和传递token
- 考虑多种客户端架构场景
- 客户可以具有不同的信任级别
缺点:
- 协议框架太宽泛,造成各种实现的兼容性和互操作性差
- 不是一个认证协议,本身并不能告诉你任何用户信息