SpringSecurity 教程
1. 简介
1.1 概念
spring security 的核心功能主要包括:
- 认证 (你是谁)
- 授权 (你能干什么)
- 攻击防护 (防止伪造身份)
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
1.2 入门案例
-
创建一个maven 项目
-
添加pom 文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>SpringSecurityDemo</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>2.4.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.4.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.4.1</version> </dependency> </dependencies> </project>
-
创建application.yml
server: port: 8082 spring: mvc: view: prefix: /templates/
-
创建controller
package com.ak.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginDemo { @RequestMapping("/mylogin") public String login(){ return "login"; } }
-
创建 登录页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> 用户名 <input type="text" ><br> 密码 <input type= "password"> </body> </html>
-
输入登录地址
http://localhost:8082/mylogin 会被拦截出现这个画面
-
输入user 用户名
密码在控制台
-
实现的时候是使用了
在UserDetailsService 里面 验证是否有次用户,
-
验证密码的过程在这里
package com.ak.test; import com.ak.demo.Demo8082Application; 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(classes = Demo8082Application.class) public class MyTest { @Test public void Test(){ PasswordEncoder encoder=new BCryptPasswordEncoder(); String pass=encoder.encode("123"); System.out.println(pass); boolean matches=encoder.matches("123", pass); System.out.println(matches); } }
1.3 自定义登录逻辑
package com.ak.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class LoginService implements UserDetailsService {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 自定义 用户登录名
if(!username.equals("admin")){
throw new UsernameNotFoundException("这是俺自己写的错误");
}
// 自定义用户密码
String password=passwordEncoder.encode("123");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
1.4 自定义登录页面
-
登录页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <form action="/login" method="post"> 用户名 <input type="text" name="username"><br> 密码 <input type= "password" name="password"> <input type="submit" value="提交"> </form> </body> </html>
自定义登录页面 name 必须是username,password ,不然就会报错。
如果不想写 username ,就需要配置
-
配置类
package com.ak.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; import org.springframework.stereotype.Controller; @Configuration public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致, .loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面 http.authorizeRequests() .antMatchers("/myLoginController").permitAll()// 放行myLoginController .anyRequest().authenticated(); http.csrf().disable(); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
-
controller 层
package com.ak.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginDemo { @RequestMapping("/myLoginController") public String myLogin(){ return "myLogin"; } @RequestMapping("/toSuccess") public String toSuccess(){ return "success"; } }
-
输入
http://localhost:8082/myLoginController
输入admin ,123 ,自定义配置文件
1.5 自定用户名参数
这两个地方一定要一致
1.5 自定义成功处理器
package com.ak.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.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Controller;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// .usernameParameter("username123")
// .passwordParameter("password123")
.loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,
.loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password
// .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.sendRedirect("https://www.baidu.com");
}
});
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.anyRequest().authenticated();
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
登录完毕后,就是自动跳转到 百度
1.6 登录失败处理器
package com.ak.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginDemo {
@RequestMapping("/myLoginController")
public String myLogin(){
return "myLogin";
}
@RequestMapping("/toSuccess")
public String toSuccess(){
return "success";
}
@RequestMapping("/toFail")
public String toFail(){
return "fail";
}
}
-
失败页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 失败的页面 <a href="/myLoginController">返回登录</a> </body> </html>
-
配置类
package com.ak.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.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler; import org.springframework.stereotype.Controller; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Configuration public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // .usernameParameter("username123") // .passwordParameter("password123") .loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致, .loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password // .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.sendRedirect("https://www.baidu.com"); } }).failureHandler(new ForwardAuthenticationFailureHandler("/toFail")); http.authorizeRequests() .antMatchers("/myLoginController").permitAll()// 放行myLoginController .anyRequest().authenticated(); http.csrf().disable(); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
1.7 认证
anyRequest
所有请求
antMatchers
任何匹配的请求
regexMatchers
正则匹配请求
1.8 授权
基于权限配置
-
修改页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 登录成功的主页面<br> <a href="/Vip1">拥有vip1 才能看的</a><br> <a href="/Vip2">拥有vip2 才能看的</a> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 这是 vip1 角色才能看见的信息 </body> </html>
-
修改Service
package com.ak.demo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; 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.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class LoginService implements UserDetailsService { @Autowired PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 自定义 用户登录名 if(!(username.equals("admin")||username.equals("vip1")||username.equals("vip2"))){ throw new UsernameNotFoundException("这是俺自己写的错误"); } if(username.equals("vip1")){ // 自定义用户密码 String password=passwordEncoder.encode("123"); return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,vip1")); } if(username.equals("vip2")){ // 自定义用户密码 String password=passwordEncoder.encode("123"); return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,vip2")); } // 自定义用户密码 String password=passwordEncoder.encode("123"); return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
现在有三个用户
vip1,vip2,admin.
vip1.html 只能 vip1看
vip2 只能vip2 看
admin 看不了 vip1,vip2页面
基于角色权限配置
package com.ak.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.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler;
import org.springframework.stereotype.Controller;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// .usernameParameter("username123")
// .passwordParameter("password123")
.loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,
.loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password
.successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面
// .successHandler(new AuthenticationSuccessHandler() {
// @Override
// public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// httpServletResponse.sendRedirect("https://www.baidu.com");
// }
// }).failureHandler(new ForwardAuthenticationFailureHandler("/toFail"));
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.antMatchers("/Vip1").hasAuthority("vip1")
.antMatchers("/Vip2").hasAuthority("vip2")
.antMatchers("/**/*.png").permitAll()
.antMatchers("/toRole").hasRole("abc")
.anyRequest().authenticated();
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
-
修改页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 登录成功的主页面<br> <a href="/Vip1">拥有vip1 才能看的</a><br> <a href="/Vip2">拥有vip2 才能看的</a> <br> <a href="/toRole">拥有abc 角色的人才能访问</a> </body> </html>
基于ip 地址
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.antMatchers("/Vip1").hasAuthority("vip1")
.antMatchers("/Vip2").hasAuthority("vip2")
.antMatchers("/**/*.png").permitAll()
.antMatchers("/toRole").hasRole("abc")
.antMatchers("/toRole").hasIpAddress("127.0.0.1")
.anyRequest().authenticated();
基于access 权限配置
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";
一共就这几种
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.antMatchers("/Vip1").hasAuthority("vip1")
.antMatchers("/Vip2").hasAuthority("vip2")
.antMatchers("/**/*.png").permitAll()
.antMatchers("/toRole").hasRole("abc")
.antMatchers("toRole").access("hasRole('abc')")
//.antMatchers("/toRole").hasIpAddress("127.0.0.1")
.anyRequest().authenticated();
1.9 注解
@Secured
判断用户是否由此角色
package com.ak.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
/**
* 用户类
*/
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Demo8082Application {
public static void main(String[] args) {
SpringApplication.run(Demo8082Application.class, args);
}
}
- 修改 配置类
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.anyRequest().authenticated();
-
修改 Controller
@Secured("ROLE_abc") @RequestMapping("/toRole") public String toRole(){ return "Role"; }
@PreAuthorize
判断是否拥有权限
-
修改application
package com.ak.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; /** * 用户类 */ @SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class Demo8082Application { public static void main(String[] args) { SpringApplication.run(Demo8082Application.class, args); } }
-
修改controller
package com.ak.demo.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginDemo { @RequestMapping("/myLoginController") public String myLogin(){ return "myLogin"; } @RequestMapping("/toSuccess") public String toSuccess(){ return "success"; } @RequestMapping("/toFail") public String toFail(){ return "fail"; } @PreAuthorize("hasAuthority('vip1')") @RequestMapping("/Vip1") public String toVip1(){ return "vip1"; } @PreAuthorize("hasAuthority('vip2')") @RequestMapping("/Vip2") public String toVip2(){ return "vip2"; } // @Secured("ROLE_abc") @PreAuthorize("hasRole('abc')") @RequestMapping("/toRole") public String toRole(){ return "Role"; } }
2. Security 原理分析
2.1 SpringSecurity 过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:
- WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
- SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
- HeaderWriterFilter:用于将头信息加入响应中。
- CsrfFilter:用于处理跨站请求伪造。
- LogoutFilter:用于处理退出登录。
- UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自
/login
的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为username
和password
,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。- DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
- BasicAuthenticationFilter:检测和处理 http basic 认证。
- RequestCacheAwareFilter:用来处理请求的缓存。
- SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
- AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
- SessionManagementFilter:管理 session 的过滤器
- ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
- FilterSecurityInterceptor:可以看做过滤器链的出口。
- RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
2.2 SpringSecurity 流程图
先来看下面一个 Spring Security 执行流程图,只要把 SpringSecurity 的执行过程弄明白了,这个框架就会变得很简单:
流程说明
- 客户端发起一个请求,进入 Security 过滤器链。
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
3. Security 配置
在 WebSecurityConfigurerAdapter
这个类里面可以完成上述流程图的所有配置
3.1 配置类伪代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
.and().authorizeRequests().antMatchers("/test").hasRole("test")
.anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
.and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable();
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
}
}
3.2 配置简介
- configure(AuthenticationManagerBuilder auth)
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
- configure(WebSecurity web)
这个配置方法用于配置静态资源的处理方式,可使用 Ant 匹配规则。
- configure(HttpSecurity http)
这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说:
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
这是配置登录相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()代表任意用户可访问。
http
.authorizeRequests()
.antMatchers("/test").hasRole("test")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
以上配置是权限相关的配置,配置了一个 /test
url 该有什么权限才能访问, anyRequest() 表示所有请求,authenticated() 表示已登录用户才能访问, accessDecisionManager() 表示绑定在 url 上的鉴权管理器
为了对比,现在贴出另一个权限配置清单:
http.authorizeRequests()
.antMatchers("/tets_a/**","/test_b/**").hasRole("test")
.antMatchers("/a/**","/b/**").authenticated()
.accessDecisionManager(accessDecisionManager())
我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意 url 上;而且可以硬编码各种 url 权限:
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
登出相关配置,这里配置了登出 url 和登出成功处理器:
http
.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
上面代码是配置鉴权失败的处理器。
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore 加在对应的过滤器之前,addFilterAfter 加在对应的过滤器之后,addFilterAt 加在过滤器同一位置,事实上框架原有的 Filter 在启动 HttpSecurity 配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用 addFilterAt 方法插入的 Filter ,会在这个位置上的原有 Filter 之前执行。
注:关于 HttpSecurity 使用的是链式编程,其中 http.xxxx.and.yyyyy
这种写法和 http.xxxx;http.yyyy
写法意义一样。
- 自定义 AuthenticationManager 和 AccessDecisionManager
重写 authenticationManagerBean() 方法,并构造一个 authenticationManager:
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider()));
return authenticationManager;
}
我这里给 authenticationManager 配置了两个认证器,执行过程参考流程图。
定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明:
public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new MyExpressionVoter(),
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。
4. Security 权限系统
- UserDetails
Security 中的用户接口,我们自定义用户类要实现该接口。
- GrantedAuthority
Security 中的用户权限接口,自定义权限需要实现该接口:
public class MyGrantedAuthority implements GrantedAuthority {
private String authority;
}
authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_
前缀,比如我们的配置 authorizeRequests().antMatchers("/test").hasRole("test")
,配置了一个 test
权限但我们存储的权限字段(authority)应该是 ROLE_test
。
- UserDetailsService
Security 中的用户 Service,自定义用户服务类需要实现该接口:
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return.....
}
}
loadUserByUsername的作用在上文中已经说明,就是根据用户名查询用户对象。
- SecurityContextHolder
用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。
手动填充 SecurityContextHolder 示例:
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);
对于使用 token 鉴权的系统,我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。
5. Security 扩展
Security 可扩展的有
- 鉴权失败处理器
- 验证器
- 登录成功处理器
- 投票器
- 自定义token处理过滤器
- 登出成功处理器
- 登录失败处理器
- 自定义 UsernamePasswordAuthenticationFilter
- 鉴权失败处理器
Security 鉴权失败默认跳转登录页面,我们可以实现 AccessDeniedHandler 接口,重写 handle() 方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中。
- 验证器
实现 AuthenticationProvider 接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点。
- 登录成功处理器
在 Security 中验证成功默认跳转到上一次请求页面或者路径为 "/" 的页面,我们同样可以自定义:继承 SimpleUrlAuthenticationSuccessHandler 这个类或者实现 AuthenticationSuccessHandler 接口。我这里建议采用继承的方式,SimpleUrlAuthenticationSuccessHandler 是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。
- 投票器
投票器可继承 WebExpressionVoter 或者实现 AccessDecisionVoter接口;WebExpressionVoter 是 Security 默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 上文;
注意:投票器 vote 方法返回一个int值;-1代表反对,0代表弃权,1代表赞成;投票管理器收集投票结果,如果最终结果大于等于0则放行该请求。
- 自定义token处理过滤器
自定义 token 处理器继承自 OncePerRequestFilter 或者 GenericFilterBean 或者 Filter 都可以,在这个处理器里面需要完成的逻辑是:获取请求里的 token,验证 token 是否合法然后填充 SecurityContextHolder ,虽然说过滤器只要添加在投票器之前就可以,但我这里还是建议添加在 http.addFilterAfter(new MyFittler(), LogoutFilter.class);
- 登出成功处理器
实现LogoutSuccessHandler接口,添加到配置的方式参考上文。
- 登录失败处理器
登录失败默认跳转到登录页,我们同样可以自定义。继承 SimpleUrlAuthenticationFailureHandler 或者实现 AuthenticationFailureHandler,建议采用继承。
- 自定义UsernamePasswordAuthenticationFilter
我们自定义UsernamePasswordAuthenticationFilter可以极大提高我们 Security的灵活性(比如添加验证验证码是否正确的功能)。
我们直接继承 UsernamePasswordAuthenticationFilter ,然后在配置类中初始化这个过滤器,给这个过滤器添加登录失败处理器,登录成功处理器,登录管理器,登录请求 url 。
这里配置略微复杂,贴一下代码清单
初始化过滤器:
MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter(redisService);
myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/sign_in");
myUsernamePasswordAuthenticationFilter.setAuthenticationManager(getAuthenticationManager());
return myUsernamePasswordAuthenticationFilter;
}
添加到配置:
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
6. 总结
对于 Security 的扩展配置关键在于 configure(HttpSecurityhttp)
方法;扩展认证方式可以自定义 authenticationManager
并加入自己验证器,在验证器中抛出异常不会终止验证流程;扩展鉴权方式可以自定义 accessDecisionManager
然后添加自己的投票器并绑定到对应的 url(url 匹配方式为 ant)上,投票器 vote(Authenticationauthentication,FilterInvocationfi,Collection<ConfigAttribute>attributes)
方法返回值为三种:-1 0 1,分别表示反对弃权赞成。
对于 token 认证的校验方式,可以暴露一个获取的接口,或者重写 UsernamePasswordAuthenticationFilter
过滤器和扩展登录成功处理器来获取 token,然后在 LogoutFilter
之后添加一个自定义过滤器,用于校验和填充 SecurityContextHolder。
另外,Security 的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回 json ,那么就需要重写各个处理器了。
SpringOauth教程
1. 简介
1.1 oauth2 概念
首先声明oauth2是一种协议规范,spring-security-oauth2是对他的一种实现。其次,还有shiro实现,自己根据规范编写代码的实现方式。主流的qq,微信等第三方授权登录方式都是基于oauth2实现的。 oauth2的认证方式有授权码,简单,账户密码,客户端等方式,具体请自行百度不做过多的阐述。 本文基于授权码方式实现
oauth生态设计的范围很大,可以说是一种解决方案,它有“第三方客户端(web服务,APP服务)”、“用户”、“认证服务器”、“资源服务器”等部分。认证流程如下图:
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
架构图
验证流程
spring security oauth2源码架构图
1.2 模式
oauth2根据使用场景不同,分成了4种模式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
2. 入门
2.1 授权码模式
项目结构如下
新建pom 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>Hoxton.BUILD-SNAPSHOT</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
准备UserDetalService
package com.example.demo1.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;
import org.springframework.stereotype.Service;
@Service
public class LoginService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 自定义 用户登录名
if(!username.equals("admin")){
throw new UsernameNotFoundException("这是俺自己写的错误");
}
String password=passwordEncoder.encode("123");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc"));
}
}
配置类
package com.example.demo1.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.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 SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,
.loginProcessingUrl("/login");// 必须和表单提交 action 的名字 一样的,提交 username 和password
// .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.antMatchers("/oauth/**","/login/**","/logout/**").permitAll()
.anyRequest().authenticated();
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
配置授权服务器--微信,qq 的授权服务器
package com.example.demo1.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
/**
* 这个是授权服务器
* 功能类似于 微信,qq 等大场企业
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServiceConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder encoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("wechat")
.secret(encoder.encode("112233")) // 秘钥
.redirectUris("https://www.baidu.com") //重定向服务器配置
.scopes("all")// 授权范围
.authorizedGrantTypes("authorization_code");
}
}
配置资源服务器
package com.example.demo1.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/**
* 这个是类似于 微信 qq 等大场的资源
*/
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**")
.permitAll()
.anyRequest().authenticated();
}
}
配置Controller
package com.example.demo1.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object kk(Authentication authentication){
return authentication.getPrincipal();
}
}
输入网址
会登录 输入admin,123
-
会出现以下
允许以后
会跳转到百度。然后后面的code 很重要
-
打开postman
接下来输输入请求资源的地方
原理讲解
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面 URL 中,response_type
参数表示要求返回授权码(code
),client_id
参数让 B 知道是谁在请求,redirect_uri
参数是 B 接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围(这里是只读)。
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri
参数指定的网址。跳转时,会传回一个授权码,就像下面这样。
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code
参数就是授权码。
第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
上面 URL 中,client_id
参数和client_secret
参数用来让 B 确认 A 的身份(client_secret
参数是保密的,因此只能在后端发请求),grant_type
参数的值是AUTHORIZATION_CODE
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址。
第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri
指定的网址,发送一段 JSON 数据。
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
上面 JSON 数据中,access_token
字段就是令牌,A 网站在后端拿到了。
2.2 密码模式
配置类修改
package com.example.demo1.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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 SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,
.loginProcessingUrl("/login");// 必须和表单提交 action 的名字 一样的,提交 username 和password
// .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面
http.authorizeRequests()
.antMatchers("/myLoginController").permitAll()// 放行myLoginController
.antMatchers("/oauth/**","/login/**","/logout/**").permitAll()
.anyRequest().authenticated();
http.csrf().disable();
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
授权服务器修改
package com.example.demo1.config;
import com.example.demo1.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
/**
* 这个是授权服务器
* 功能类似于 微信,qq 等大场企业
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServiceConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder encoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
LoginService service;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(service);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("wechat")
.secret(encoder.encode("112233")) // 秘钥
.redirectUris("https://www.baidu.com") //重定向服务器配置
.scopes("all")// 授权范围
.authorizedGrantTypes("authorization_code","password");
}
}
输入网址
http://localhost:8082/oauth/token
结果
原理
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
第一步,A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
https://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
上面 URL 中,grant_type
参数是授权方式,这里的password
表示"密码式",username
和password
是 B 的用户名和密码。
第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
2.3 简化模式
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。
第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面 URL 中,response_type
参数为token
,表示要求直接返回令牌。
第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri
参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
https://a.com/callback#token=ACCESS_TOKEN
上面 URL 中,token
参数就是令牌,A 网站因此直接在前端拿到令牌。
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
2.4 客户端模式
最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。
第一步,A 应用在命令行向 B 发出请求。
https://oauth.b.com/token? grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
上面 URL 中,grant_type
参数等于client_credentials
表示采用凭证式,client_id
和client_secret
用来让 B 确认 A 的身份。
第二步,B 网站验证通过以后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
令牌的使用
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。
此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization
字段,令牌就放在这个字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.b.com"
上面命令中,ACCESS_TOKEN
就是拿到的令牌。
更新令牌
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://b.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN
上面 URL 中,grant_type
参数为refresh_token
表示要求更新令牌,client_id
参数和client_secret
参数用于确认身份,refresh_token
参数就是用于更新令牌的令牌。
B 网站验证通过以后,就会颁发新的令牌。
写到这里,颁发令牌的四种方式就介绍完了。下一篇文章会编写一个真实的 Demo,演示如何通过 OAuth 2.0 向 GitHub 的 API 申请令牌,然后再用令牌获取数据。
2.5 Redis 存储令牌
创建配置类
package com.example.demo1.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
public class RedisConfig {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}
授权服务器
package com.example.demo1.config;
import com.example.demo1.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* 这个是授权服务器
* 功能类似于 微信,qq 等大场企业
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServiceConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder encoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
TokenStore tokenStore;
@Autowired
LoginService service;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(service)
.tokenStore(tokenStore);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("wechat")
.secret(encoder.encode("112233")) // 秘钥
.redirectUris("https://www.baidu.com") //重定向服务器配置
.scopes("all")// 授权范围
.authorizedGrantTypes("authorization_code","password");
}
}
输入网址
打开redis
JWT 教程
1. 介绍
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
2. SpringBoot与JWT整合
2.1 JWT的结构:
Header(头):包含令牌的类型与使用的签名算法,它会使用Base64进行编码
{
"alg": "HS265",
"typ": "JWT"
}
Payload(有效负载): 包含声明(有关用户实体和其他数据的声明),使用Base64进行编码
Base64是一种可逆的编码,因此不要在负载里存入敏感数据!
{
"id": "1",
"name": "BLU",
"admin": true
}
Signature(签名):使用编码后的header和payload以及一个指定密钥,然后使用header中指定的算法(HS265)进行签名.
签名的作用是保证JWT没有被篡改过
负载
iss: jwt 的签发者
sub: jwt 所面向的用户
aud:接受jwt 的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf:定义在什么时间之前,该jwt 不可用的
iat: jwt的签发时间
jti:jwt 的唯一身份识别,主要用来作为一次性token,从而回避重放攻击
有很多公司在做jwt ,因为jwt 是标准,并不是实现,所以需要公司实现,比较有名的
auth0,java-jwt,jjwt
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
2.2 JWT的使用测试:
1) java-jwt 版本
- 依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
- 测试:
@Test
void getToken() {
//HashMap<String,Object> map = new HashMap<String, Object>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 60);
String token = JWT.create()
//.withHeader(map)
//设置Payload
.withClaim("userId", 10)
.withClaim("username", "BLU")
//设置token过期时间(60s)
.withExpiresAt(instance.getTime())
//设置签名
.sign(Algorithm.HMAC256("!@#$%^&*"));
System.out.println(token);
}
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDA3NDA2MTUsInVzZXJJZCI6MTAsInVzZXJuYW1lIjoiQkxVIn0.0re-tA4dQm4blGhn1DvpnUl7Lrz_EWXwn8LfRbWQXCU
@Test
void TokenVerify() {
//创建验证对象
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("!@#$%^&*")).build();
DecodedJWT verify = verifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDA3NDA2MTUsInVzZXJJZCI6MTAsInVzZXJuYW1lIjoiQkxVIn0.0re-tA4dQm4blGhn1DvpnUl7Lrz_EWXwn8LfRbWQXCU");
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaims().get("username").asString());
System.out.println("过期时间:"+verify.getExpiresAt());
}
10
BLU
过期时间:Tue Sep 22 10:10:15 CST 2020
2) jjwt 版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo3</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
验证
package com.example.demo3;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
class Demo3ApplicationTests {
@Test
void contextLoads() {
JwtBuilder jwtBuilder= Jwts.builder()
// 用户的id
.setId("001")
// 接受用户的角色信息
.setSubject("Rose")
// 签发时间
.setIssuedAt(new Date())
// 采用的算法
.signWith(SignatureAlgorithm.HS256,"xxxx");
String token=jwtBuilder.compact();
System.out.println(token);
}
}
结果
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDEiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA5NTcxNzg1fQ.pqau27EgQhVtqamer2_8z-gBQeATGNveBuUAyVlSac4
检验
@Test
public void verfity(){
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDEiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA5NTcxNzg1fQ.pqau27EgQhVtqamer2_8z-gBQeATGNveBuUAyVlSac4";
Claims claims= Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
}
结果
001
Rose
Sat Jan 02 15:16:25 CST 2021
过期时间
@Test
void contextLoads() {
JwtBuilder jwtBuilder= Jwts.builder()
// 用户的id
.setId("001")
// 接受用户的角色信息
.setSubject("Rose")
// 签发时间
.setIssuedAt(new Date())
// 采用的算法
.signWith(SignatureAlgorithm.HS256,"xxxx")
.setExpiration(new Date(System.currentTimeMillis()+60*1000));
String token=jwtBuilder.compact();
System.out.println(token);
}
自定义claim
package com.example.demo3;
import io.jsonwebtoken.*;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
class Demo3ApplicationTests {
@Test
void contextLoads() {
JwtBuilder jwtBuilder= Jwts.builder()
// 用户的id
.setId("001")
// 接受用户的角色信息
.setSubject("Rose")
// 签发时间
.setIssuedAt(new Date())
// 采用的算法
.signWith(SignatureAlgorithm.HS256,"xxxx")
// .setExpiration(new Date(System.currentTimeMillis()+60*1000))
.claim("name","joker")
.claim("login","sadas");
String token=jwtBuilder.compact();
System.out.println(token);
}
@Test
public void verfity(){
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDEiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA5NTcyODU4LCJuYW1lIjoiam9rZXIiLCJsb2dpbiI6InNhZGFzIn0.-Z2YMakhk6pKa27xyEFgGb5Jjm0ebMUEMpLSFA42c7A";
Claims claims= Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
System.out.println(claims.get("name"));
System.out.println(claims.get("login"));
}
}
2.3 SpringBoot与JWT整合:
- 依赖
<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
- 配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
mybatis.type-aliases-package=com.blu.entity
mybatis.mapper-locations=classpath:mapper/*.xml
logging.level.com.blu.dao=debug
- 数据库user表:
- 实体类:
package com.blu.entity;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain=true)
public class User {
private String id;
private String name;
private String password;
}
- UserDao:
package com.blu.dao;
import org.apache.ibatis.annotations.Mapper;
import com.blu.entity.User;
@Mapper
public interface UserDao {
User login(User user);
}
- UserMapper:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blu.dao.UserDao">
<select id="login" parameterType="User" resultType="User">
select * from user where name= #{name} and password= #{password}
</select>
</mapper>
- UserService:
package com.blu.service;
import com.blu.entity.User;
public interface UserService {
User login(User user);
}
- UserServiceImpl:
package com.blu.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.blu.dao.UserDao;
import com.blu.entity.User;
import com.blu.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User login(User user) {
User userDB = userDao.login(user);
if(userDB!=null) {
return userDB;
}
throw new RuntimeException("认证失败!");
}
}
- JWT的工具类封装:
package com.blu.utils;
import java.util.Calendar;
import java.util.Map;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JWTUtils {
private static final String SIGN= "!@#$%^&*123456789";
/**
* 生成Token
*/
public static String getToken(Map<String,String> map) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7);
//创建JWTBuilder
JWTCreator.Builder builder = JWT.create();
//设置payload
map.forEach((k,v)->{
builder.withClaim(k, v);
});
//设置过期时间和签名,生成token
String token = builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SIGN));
return token;
}