课程: https://www.bilibili.com/video/BV1ZN411Q7d8 p1 到 p30
Spring Security 介绍提供认证、授权、加密功能的安全框架。
1、用户凭证信息处理 UserDetailServiceUserDetailService
public interface UserDetailsService {
// 根据 username(唯一标识) 加载用户信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetail 提供核心用户信息
public interface UserDetails extends Serializable {
Collection extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
实现类:User,注意,这里的 User 类是 spring-security 官方提供的
2、加密规则 Passwordencoderpublic interface PasswordEncoder {
// 对原始密码(前端传来的“明文”,是前端md5的结果)进行编码。
// 通常,好的编码算法应用 SHA-1 或更大的哈希值与 8 字节或更大的随机生成的盐值相结合。
String encode(CharSequence rawPassword);
// 检查 明文密码是否与密文密码 匹配
boolean matches(CharSequence rawPassword, String encodedPassword);
// 生成的密文是否需要再次被加密。用来判断密文安全性
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
很多实现类,官方推荐 BCryptPasswordEncoder
@Test
void t1() {
PasswordEncoder pa = new BCryptPasswordEncoder();
String secret = pa.encode("engure");
System.out.println(secret);
System.out.println(pa.matches("engure", secret));
System.out.println(pa.matches("123", secret));
}
注意:容器中没有 Passwordencoder 对象,需要注入
3、自定义登录 1. 自定义登录逻辑(身份信息)@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPE() {
return new BCryptPasswordEncoder();
}
}
定义登录逻辑 — 加载指定用户的信息
这里为了简单假设只有一个 admin 用户也可以定义多个用户,也可以连接数据库并从中检索用户
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pe;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!StringUtils.hasLength(username)) throw new UsernameNotFoundException("invalid username!");
if (!"admin".equals(username)) throw new UsernameNotFoundException("user not exist!");
String pwd = pe.encode("123");
// 返回核心用户信息,这里的 User 是 org.springframework.security.core.userdetails 包下的!
return new User(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,xyz"));
}
}
2. 自定义登录页面及错误处理
@Override
protected void configure(HttpSecurity http) throws Exception {
// 请求授权管理
http.authorizeRequests()
// 授权策略: 允许所有人访问 /login.html, /login_error.html
.antMatchers("/login.html").permitAll()
.antMatchers("/login_error.html").permitAll()
// 其他的请求都需要授权
.anyRequest().authenticated();
http.formLogin()
// 登录页面
.loginPage("/login.html")
// 登录处理
.loginProcessingUrl("/login")
// 登录成功
.successForwardUrl("/toMain")
// 登录失败
.failureForwardUrl("/toLoginError");
// 禁用 csrf 防护
http.csrf().disable();
}
3. 自定义表单中用户名和密码的参数名
体现在 UsernamePasswordAuthenticationFilter 中:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postonly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request); // 使用指定的参数从请求中获取登录名
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request); // 使用指定的或默认的参数从请求中获取密码
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
4. 自定义登录成功跳转逻辑
之前的成功跳转:
原理:验证通过,转发到一个 POST 处理器(点进 successForwardUrl() 发现是用的是原生的转发)
弊端:前后端分离项目中不能使用转发。
修改成重定向:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String redirectUrl;
public MyAuthenticationSuccessHandler(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(redirectUrl);
}
}
使用该处理器
与 4. “登录成功” 跳转逻辑 同理,定义登录失败处理器:
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String redirectUrl;
public MyAuthenticationFailureHandler(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(redirectUrl);
}
}
4、几个 API
1. anyRequest()
// 请求授权管理
http.authorizeRequests()
// 授权策略: 允许所有人访问 /login.html, /login_error.html
.antMatchers("/login.html").permitAll()
.antMatchers("/login_error.html").permitAll()
// 其他的请求都需要授权
.anyRequest().authenticated();
anyRequest() 需要放在最后
2. antMachers()public C antMatchers(String... antPatterns) {}
方法参数是可变参数,每个字符串都是一个 ant 表达式:
?:匹配一个字符*:匹配 0 个或多个字符**:匹配 0 个或多个目录
示例:放行静态资源
- 根据目录放行:antMatchers("/js*.png").permitAll()
除此之外,还有一个重载方法:
public C antMatchers(HttpMethod method, String... antPatterns) {}
第一个参数用来指定访问的方法。相同的 url 路径,只授权指定的请求方法。
3、regexMachers()public C regexMatchers(String... regexPatterns) {}
public C regexMatchers(HttpMethod method, String... regexPatterns) {}
与 antMachers() 唯一不同的是,每个参数都是一个正则表达式。
比如:regexMachers(".+[.]png").permitAll()
4. mvcmachers()在配置了项目路径的情况下使用
spring.mvc.servlet.path=/xyz
.mvcMatchers("/demo").servletPath("/xyz").permitAll()
// /xyz/demo
5、访问控制
1. 内置的访问控制方法
进入 permitAll() 方法所在的类:
public final class expressionUrlAuthorizationConfigurer2. 根据角色与权限授权> extends AbstractInterceptUrlConfigurer , H> { static final String permitAll = "permitAll"; private static final String denyAll = "denyAll"; private static final String anonymous = "anonymous"; private static final String authenticated = "authenticated"; private static final String fullyAuthenticated = "fullyAuthenticated"; private static final String rememberMe = "rememberMe";
之前配置的用户信息:(账号、密码、角色权限字符串)
其中的 admin,xyz 表示给该用户 admin 和 xyz 的权限。(注:是权限而不是角色,两者是同级关系)
按「权限」配置授权规则
antMachers().hasAuthority() // 一个权限 antMachers().hasAnyAuthority() // 任一权限
表示授权给拥有 admin 权限的用户对 docs/doc1.html 的访问权。
按「角色」配置授权规则
添加角色(与添加权限有所不同):“admin,xyz,ROLE_abc,ROLE_xYz”
以 ROLE_ 为前缀,后边的内容为角色标识角色标识 严格区分大小写
antMachers().hasRole() // 一个角色 antMachers().hasAnyRole() // 任一角色3. 根据 ip 地址授权
.antMatchers("/demo").hasIpAddress("192.168.121.2")
查看登录信息:
4. access 表达式总结授权控制:访问 xxx 资源需要满足 xxx 条件
进入“授权动作” 查看:
很多 “授权” 方法都调用了 access() 方法。仔细看,发现 access() 方法的参数是一个表达式。
该表达式描述了 “授权动作”。简单使用:
.antMatchers("/abc").permitAll()
.antMatchers("/abc").access("permitAll()")
.antMatchers("/xyz").hasAnyRole("admin", "normal")
.antMatchers("/xyz").access("hasAnyRole('admin','normal')")
.antMatchers("").hasIpAddress("127.0.0.1")
.antMatchers("").access("hasIpAddress('127.0.0.1')")
详细文档:
https://docs.spring.io/spring-security/reference/5.6.1/servlet/authorization/expression-based.html#el-common-built-in
acess 表达式优点:功能更强大。原生的授权方法(比如 permitAll(),hasIpAddress() 等)不能连着写,如果要对某一个资源添加多个权限控制则需要写多个 antMatchers().xxx,使用 access 表达式的方式支持同时写多个授权条件比如 access("hasRole('ROLE_USER') and hasIpAddress('10.10.10.3')")
高级用法:使用 access 表达式 + 自定义方法 实现授权
示例接口:
添加权限:(在UserDetailServiceImpl中)
自定义授权方法:
@Service
public class MyAccessCtrlServiceImpl implements MyAccessCtrlService {
@Override
public boolean canAccess(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails user = (UserDetails) principal;
Collection extends GrantedAuthority> authorities =
user.getAuthorities();
System.out.println("uri " + request.getRequestURI());
System.out.println("authorities " + authorities);
// 如果有 URI 代表的权限,那么可以访问这个 URI
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
使用 access 表达式配置授权规则:
.antMatchers("/access/demo").access("@myAccessCtrlServiceImpl.canAccess(request, authentication)")
6、自定义 403 响应
自定义”访问拒绝处理器“:
public class MyAccessDeniedHandler implements AccessDeniedHandler {
// 处理方法
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 自定义 403 处理逻辑
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{"status":"error", "message":"权限不足,请联系管理员!"}");
writer.flush();
writer.close();
}
}
配置到异常处理中:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
}
7、基于注解的权限控制
1. @Secured
使用 @Secured 注解,代替 .antMatchers("/secured").hasRole(“abc”)
需要开启注解使用:@EnableGlobalMethodSecurity(securedEnabled = true)可以修饰方法或类,判断用户是否有某个角色。(注意:这里是角色,而不是权限)判断的角色字符串必须以 ROLE_ 开头,并且后边的角色标识大小写敏感。通常标注在 controller 的方法上,比如:
@Secured("ROLE_abc") // 同 .antMatchers("/secured").hasRole("abc")
@ResponseBody
@GetMapping("/secured")
public String securedTest() {
return "Got secured protect!";
}
2. @PreAuthorize / @PostAuthorize
@PreAuthorize 与 @PostAuthorize:
需要开启注解支持:@EnableGlobalMethodSecurity(prePostEnabled = true)可用来修饰类或方法,在类(的方法)或方法执行前或后进行权限控制,前者常用后者不常用通常使用前者标注 controller 的方法,进行权限控制注解的参数是 access 表达式优点:更加灵活示例:
@PreAuthorize("hasRole('abc')") // 授权控制,access 授权表达式
@ResponseBody
@GetMapping("/prePostAuthorize")
public String testPrePostAuthorize() {
return "@PreAuthorize matters!";
}
注:这里基于角色的权限控制的方法 hasRole 中的参数可以以 ROLE_ 开头,即 “hasRole(‘ROLE_abc’)” 也是正确的。
8、记住我功能将用户登录的 token 持久化在数据库中,需要连接数据库
org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java
# 数据源 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=mysql spring.datasource.url=jdbc:mysql://localhost:3306/security
记住我相关配置:在 SecurityConfig 中配置
// 记住我
http.rememberMe()
// token 寿命
.tokenValiditySeconds(60 * 60 * 12 * 3)
// token 持久层对象。如果不配置默认持久化在内存中
.tokenRepository(persistentTokenRepository)
// 表单参数名
.rememberMeParameter("remember-me");
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 设置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 首次启动时为我们创建表。第二次启动要注释次行。
// jdbcTokenRepository.setCreateTableonStartup(true);
return jdbcTokenRepository;
}
修改表单:
测试:
Spring Security 可以在一些视图技术中进行控制显示效果。比如 JSP 和 thymeleaf。
Thymeleaf 对 Spring Security 的支持都放在 thymeleaf-extras-springsecurityX 中(其中 X 为 Spring Security 的版本)
https://github.com/thymeleaf/thymeleaf-extras-springsecurity 1. 在 thymeleaf 中获取属性值
org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity5
编写 thymeleaf 待解析的页面,使用提供的属性值:
Title
认证信息
登录账号:
登录账号:
凭证:
权限与角色:
客户端地址:
sessionId:
编写 controller 转发页面。访问:
相关类:
UsernamePasswordAuthenticationToken 及其父类AbstractAuthenticationTokenWebAuthenticationDetails
principle 是 UserDetails
credentials 是凭证
2. 在 thymeleaf 中进行权限判断
Title
权限判断
通过权限判断:
通过角色判断:
定义权限:
访问网页:
发现按照权限判断时只有新增和查看两个按钮,说明权限判断失败的标签不能被渲染。
10、退出登录功能默认情况下(不使用 http.logout() 时),提供 /logout 登出功能。
若要定制登出地址和登出成功地址:
查看源码:
查看 http.logout() 的返回值,LogoutConfigurer 类,
发现上边使用传递 URL 的方式来处理登出相关逻辑实际是由两个 Handler 来处理的,我们也可以通过自定义 handler 的方式处理登出和登出成功逻辑:仿写已有的 xxxHandler
分析使用的 Handler:
1、LogoutHandler 处理登出逻辑
默认使用的实现类:SecurityContextLogoutHandler,该类的实现:
处理登录逻辑:1. 让 session 失效 2. 清除认证信息。默认情况下连这都会执行
2、LogoutSuceessHandler 登出成功处理器
默认 SimpleUrlLogoutSuccessHandler,处理逻辑:
11、csrf 防护跨站请求伪造。
小 A 访问 hacker 的网站并在不知不觉中点击了某个银行的转账链接(hacker 的圈套)比如 http://xxxbank.cn/transfer?to=hacker&money=20000,如果小 A 登录该银行网站并且未注销,那么此时小 A 与银行网站的回话未结束浏览器保存回话 token,会导致点击 hacker 的链接会默认带上与银行网站的回话 token 造成 hacker 得逞。
解决方法:
将 token 放置在请求头中,排除发送请求自动携带 cookie 的问题cookie方案下的 post 表单提交要求用户携带另一个 _csrf token(spring security 的 csrf 防护)
框架学习时关闭 csrf 防护,生产环境下,需要开启防护,http.csrf().disable()
===》一般在发送请求时要求使用 post 表单的方式,需要携带 “个性化信息”,让服务器知道我们的身份,登录示例:
我们额外携带的信息不能是 cookie,上例是发送请求时需要携带嵌在网页表单中的 crsf token。



