- 对于一个核心框架而言,核心功能就两个
- 认证
- 用户能否登录进来
- 授权
- 用户是否有权限进行某种操作
- 认证
Spring Security 简介
security自己内置的用户逻辑。如果需要从db中检索用户的话,需要进行实现并重写方法。
BcryptPasswordEncoder简介
是官方推荐的密码解析器,平时多使用这个解析器
- 是对bcrypt强散列方法的具体实现,是基于hash算法实现的单向加密,可以通过strength控制加密强度,默认10 ```java package com.addicated.se_demo;
import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest class SeDemoApplicationTests {
@Test
void contextLoads() {
PasswordEncoder pw = new BCryptPasswordEncoder();
// 进行加密
String encode = pw.encode("123");
System.out.println(encode);
// 比较密码
boolean matches = pw.matches("123", encode);
System.out.println("--------一个分割线------------");
System.out.println(matches);
}
}
<a name="V3daE"></a>
# 自定义登录逻辑
- 通过对上面两个组件的了解学习,就可以完成自定义登录逻辑,贴合自身业务
<a name="0kNdO"></a>
## Security 配置类
- 注意点
- 在spring中使用passwordEncoder的时候,需要将其di到spring容器中进行管理,之后直接取用
```java
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
- 创建service方法,实现UserDetailService接口,并重写loadUserByUsername方法 ```java package com.addicated.se_demo.service;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder;
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1 根据用户名去db查询,如果不存在 ,抛出usernameNotFoundException
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
//2 比较密码 (需要注意,用户注册的密码肯定是密闻的,由pw进行加密过的) 如果匹配成功,返回userdetails
String password = passwordEncoder.encode("123");
// 查库部分并没有写明,重点在如何使用,而未深究
return new User(username, password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
<a name="ztwT6"></a>
# 自定义登录页面
- security支持自定义登录页面,毕竟他自带的很丑,接下来看怎么进行配置自定义登录
<a name="1BGaF"></a>
## 修改security配置类 - 成功页面
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1608527/1620548060793-d664036b-b669-4818-ba66-241d6e08e0da.png#align=left&display=inline&height=686&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1372&originWidth=2586&size=2553578&status=done&style=none&width=1293)
- 继承 WebSecurityConfigurerAdapter 并重写configure方法 需要注意的是,要重写参数为 HttpSecurity http的那个方法
```java
package com.addicated.se_demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 自定义登录页面 ,, 页面
.loginPage("/login.html")
// 调用的接口 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 设定登录成功后的跳转页面 但是支持post请求
.successForwardUrl("/main.html");
// 授权
http.authorizeRequests()
// 放行所有login页面的请求 如果出现无限重定向的问题的话即是,login页面的请求都被拦截转发了
.antMatchers("/login.html").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
// 关掉跨站
http.csrf().disable();
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
失败页面跳转
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 自定义登录页面 ,, 页面
.loginPage("/login.html")
// 调用的接口 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 设定登录成功后的跳转页面 但是支持post请求
.successForwardUrl("/main.html")
// 设定登录失败后的跳转页面,同样只支持 post请求
.failureForwardUrl("/toError.html");
// 授权
http.authorizeRequests()
// 放行所有到 toeeror页面的请求
.antMatchers("/toError.html").permitAll()
// 放行所有login页面的请求 如果出现无限重定向的问题的话即是,login页面的请求都被拦截转发了
.antMatchers("/login.html").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
// 关掉跨站
http.csrf().disable();
自定义设置请求账户和密码的参数名
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 自定义入参
.usernameParameter("username123")
.passwordParameter("password123")
// 自定义登录页面 ,, 页面
.loginPage("/login.html")
// 调用的接口 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 设定登录成功后的跳转页面 但是支持post请求
.successForwardUrl("/main.html")
// 设定登录失败后的跳转页面,同样只支持 post请求
.failureForwardUrl("/toError.html");
自定义登录成功处理器
- security中有默认的登录成功处理器,且会默认去调用,如果想要实现自定义的话,需要自行实现接口,重写方法,下看代码 ```java
// 自定义sucesshandler 实现 AuthenticationSuccessHandler并重写方法 public class MySuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MySuccessHandler(String url) { // 对url来个构造方法
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(url); // 仅作为例子,自定义successhandler实现自定义登录成功跳转
}
}
<a name="OF6E3"></a>
## 使用上
```java
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 自定义入参
.usernameParameter("username123")
.passwordParameter("password123")
// 自定义登录页面 ,, 页面
.loginPage("/login.html")
// 调用的接口 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 设定登录成功后的跳转页面 但是支持post请求
// .successForwardUrl("/main.html") // 注释掉原生的,使用自定义的来进行跳转
.successHandler(new MySuccessHandler("www.baidu.com"))
// 设定登录失败后的跳转页面,同样只支持 post请求
.failureForwardUrl("/toError.html");
自定义失败成功处理器
与success同理failureForwardUrl 其实也是调用了security中的 failureHandler ,如法炮制去重写一个failureHandler即可
anyRequest()
所有的请求。security 中对对于授权是有先后顺序的,anyRequest是必须要放在最后的,因为授权拦截是从上向下进行拦截。只要记住放在最后面即可
表示匹配所有的请求,一般情况下都会使用,设置全部内容都需要进行验证
antMatcher()
参数是不定项参数,每个参数是一个ant表达式,用于匹配url规则。
- 规则如下
- ? 匹配一个字符
- 匹配0个或者多个字符
- ** 匹配0个或者多个目录
在实际项目中经常需要放行所有静态资源,下面掩饰放行js文件夹下所有文件
.antMatchers("/js/**","/css/**").permitAll()
还有一种配置方式是只要是 js文件都放行
.antMatchers("/**/*.js").permitAll()
regexMatchers()
支持正则表达式的匹配方式
同时matchers还支持定义请求方法, 上面的antmatcher()同样可用
.regexMatchers(HttpMethod.GET,"/demo").permitAll()
mvxMathcers()
mvc匹配
servletPath 为在application中配置的所有controller访问路径的前缀。
.mvcMatchers("/demo").servletPath("/xxxx")
权限控制
hasAuthority() - 参数对大小写敏感
相关statuscode 403 一般来说是权限不够。
使用
// 授权
http.authorizeRequests()
// 放行所有到 toeeror页面的请求
.antMatchers("/toError.html").permitAll()
// 放行所有login页面的请求 如果出现无限重定向的问题的话即是,login页面的请求都被拦截转发了
.antMatchers("/login.html").permitAll()
.regexMatchers(HttpMethod.GET,"/demo").permitAll()
.antMatchers("/leve1").hasAuthority("leve1")
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
leve1 则是在创建用户,或者保存在db中的用户权限信息,比如之前在程序中写死的一个例子,
-
hasAnyAuthority()
可以进行多个权限判断
// 授权
http.authorizeRequests()
// 放行所有到 toeeror页面的请求
.antMatchers("/toError.html").permitAll()
// 放行所有login页面的请求 如果出现无限重定向的问题的话即是,login页面的请求都被拦截转发了
.antMatchers("/login.html").permitAll()
.regexMatchers(HttpMethod.GET,"/demo").permitAll()
.antMatchers("/leve1").hasAnyAuthority("leve1","leve2")
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
角色控制
角色在进行配置的时候必须使用 ROLE_ 为前缀,下例
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1 根据用户名去db查询,如果不存在 ,抛出usernameNotFoundException
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
//2 比较密码 (需要注意,用户注册的密码肯定是密闻的,由pw进行加密过的) 如果匹配成功,返回userdetails
String password = passwordEncoder.encode("123");
return new User(username, password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));
}
而在进行使用的时候不能带ROLE前缀
hasRole()
-
hasAnyRole()
多个角色进行匹配,
.antMatchers("/login.html").hasRole("abc")
.antMatchers("/login.html").hasAnyRole("abc","ABC")
基于IP去进行控制
hasIpAddress(ip)
.antMatchers("/login.html").hasIpAddress("127.0.0.1")
-
自定义403处理页面
与之前的自定义成功,失败相一致,同样需要实现并重写一个接口,这次的接口为 AccessDeniedHandler
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 设置响应编码
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 设置返回json格式
response.setHeader("Content-Type","application/json;charset=utf-8");
// 传给前端的响应内容
PrintWriter writer = response.getWriter();
writer.write("{status:error,msg:权限不足,请联系管理员}");
writer.flush();
writer.close();
}
}
之后同样在securityconfig中使用 ```java
@Autowired private MyAccessDeniedHandler myAccessDeniedHandler;
// 异常处理 可以直接使用spirng注入之后方便使用而不要像下面这样new http.exceptionHandling() .accessDeniedHandler(new MyAccessDeniedHandler()); .accessDeniedHandler(myAccessDeniedHandler);
<a name="1RpK0"></a>
# 基于Access的访问控制
- 与上面的几种类似,或者说上面几种控制方式的底层就是acess,所有的权限控制本质上是在调用access,access有专门的表达式语法,需要在官网上查看
- ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1608527/1620656410947-aea0c59c-e0db-4cef-b05f-b218fee22715.png#align=left&display=inline&height=527&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1054&originWidth=1666&size=1137373&status=done&style=none&width=833)
<a name="IGCFN"></a>
# 基于注解的访问控制
- spring security除了配置的方式同时还支持注解的方式进行权限配置,但是默认是不开启的,需要使用注解去开启
- 这些注解可以写到Service接口或者方法上,也可写到controller或者controller的方法上,通常情况下都是写在控制器方法上的,控制接口url是否允许被访问
<a name="c2931"></a>
## 开启注解权限控制
- 找到springboot启动类,添加注解
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1608527/1620658788547-cac549b4-2b86-442a-b265-191900cea1df.png#align=left&display=inline&height=328&margin=%5Bobject%20Object%5D&name=image.png&originHeight=656&originWidth=1446&size=323138&status=done&style=none&width=723)
<a name="cZIs6"></a>
## @Secuted
- 专门用于判断是否具有角色,能写在方法或者类上,参数要以ROLE开头 ,其参数就是角色名称 相当于前面配置的hasrole
```java
@Controller
public class LoginController {
@Secured("ROLE_abc")
@RequestMapping("/login")
public String login() {
return "redirect:main.html";
}
}
@PreAuthorize
- 与下面那个注解同为方法或者类级别的注解
表示访问方法或者类在执行之前先判断权限,大多数情况都是使用这个注解,注解的参数和access()方法参数取之相同,都是权限表达式
@PreAuthorize
-
开启 PreAuthorize PreAuthorize 注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
同样是在启动类上添加注解,之后就可以在controller类上进行使用
RemberMe 功能实现
spring security 中 memberme为记住我功能,用户只需勾选remember-me复选框取值为true,spring security 会自动吧用户信息存储到数据源中,之后可以不登录进行访问
添加依赖
需要以来spring-jdbc 但是如果使用mybatis框架则不需要重复添加。
配置数据源
接口有两个实现类,分别为 基于内存,和基于数据源存储两种方式
进行配置之前要先定义一个bean并注入给spring
@Bean
public PersistentTokenRepository persistentTokenRepository(){
// 定义bean并交给spring容器管理
// 设置数据源
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(数据源对象);
// 自动建表,第一次启动的时候自动开启,第二次启动的时候需要吧这行配置关掉
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
之后在securityConfig类中进行配置
// FIXME rememberMe 功能实现
http.rememberMe()
// 设置数据源 设置之前要注意先进行di注入
.tokenRepository(jdbcTokenRepository)
.rememberMeParameter() // 同之前username password一样的设置参数名称
.tokenValiditySeconds() // 超时时间
.userDetailsService(自定义的登录逻辑注入)
登出操作 logout
推荐直接使用security原生支持的logout,进行修改之后很多地方需要连带修改
http.logout()
.logoutUrl("登出的url")
.logoutSuccessUrl("登出成功之后跳转页面url");
CSRF
跨站请求伪造,通过伪造用户请求访问受信任站点的非法请求访问
- 跨域:只要网络协议,ip地址,端口中任何一个不同就算作为跨域请求。
客户端与服务端进行交互的时候,由于http协议本身是无装他死协议,索引一如cookie进行记录客户端身份,在cookie中会存放sessionid用来识别客户端的身份,在跨域的情况下,sessionid可能被第三方恶意劫持通过这个session id 向服务端发送请求时,服务端会认为这个请求是合法的,kennel发生很多意想不到的问题
Spring Security中的csrf
从 spring security4开始csrf防护默认开启,默认会拦截请求,进csrf处理,csrf为了保证不是其他第三方网站访问,要求访问时懈怠参数名为 _csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
Oauth2认证
oauth协议为用户资源的授权提供了一个安全的,开放又简易的标准,同时,任何第三方都可以使用oatuh认证服务,任何服务提供商都可以实现自身的oauth认证服务,
- oauth目前发展到2.0版本,1.0版本过于复杂,
角色
客户端
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如 android客户端,web客户端,微信客户端等
资源拥有者
-
授权服务器 (也叫认证服务器)
用来对资源拥有的身份进行认证,对访问资源进行授权,客户端想要访问资源需要通过认证服务器由资源拥有者授权之后可访问。
资源服务器
存储资源的服务器,比如网站用户管理服务器存储了网站用户信息,网站相册服务器存储了用户的相册信息,微信的资源服务存储了微信的用户信息等,客户端最终访问资源服务器获取资源信息
常用术语
客户凭证 client credentials 客户端的clientid和密码用于认证客户
- 令牌 tokens 授权服务器在接收到客户请求后办法的访问令牌
作用域 scopes 客户请求访问令牌时,由资源拥有者额外制定的细分权限
令牌类型
授权码 仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌
- 访问令牌 用于代表一个用户或服务直接去访问受保护的资源
- 刷新令牌 用于去授权服务器获取一个刷新访问令牌
- BearerToken 不管谁拿到token都可以访问资源,类似先进
Prof of Possession(pop) token :可以检验client是否对token由明确的拥有权
特点
优点
- 更安全,客户端不接触用户密码,服务器端更易集中保护。
- 广泛传播并被持续采用
- 短寿命和封装的token
- 资源服务器和授权服务器结偶
- 缺点