本文仅为个人学习笔记,通过简单的框架搭建来初步学习Spring Security及OAuth2,文中部分方法注解已过时,但仅为学习使用
认证:认证用户的合法性,如登录
授权:登录后,用户具有什么操作权限
会话:将用户认证信息保存起来,避免重复认证(常见方式session,token等)
整体的思想:登录认证获得一个权限标识与权限相关信息,存入会话中(session,结合redis等),使得在有效期内无需重复登录或者请求接口校验用户的权限
RBAC:Role-Based Access Control 基于角色的访问控制,由用户,角色,权限组成
简单来说就是一个用户,具有什么角色,每个角色具有什么权限,从而决定改用户具有什么权限
pom增加依赖
org.springframework.boot
spring-boot-starter-thymeleaf
org.projectlombok
lombok
true
yml配置文件
server:
port: 5555
spring:
application:
name: security-demo
thymeleaf:
prefix: classpath:templates/
suffix: .html
实体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String userId;
private String userName;
private String password;
private List roles = new ArrayList();
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private String roleId;
private String roleName;
private List resources = new ArrayList();
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Resource {
private String resourceId;
private String resourceName;
private String resourceType;
}
controller
@Controller
@RequestMapping("/auth")
public class LoginController {
@Resource
private AuthService authService;
@GetMapping
public String test(){
return "success";
}
//首页
@RequestMapping("/index")
public String index(){
return "index";
}
//登录
@RequestMapping("/login")
public String login(String userName,String password, HttpServletRequest request){
User query = new User();
query.setUserName(userName);
query.setPassword(password);
User userResult = this.authService.getUser(query);
if(null == userResult){
return "failed";
}
request.getSession().setAttribute("current_user",userResult);
return "success";
}
//获取当前用户
@PostMapping("/getCurrUser")
public User getCurrentUser(HttpSession session){
return (User)session.getAttribute("current_user");
}
//登出
@PostMapping("logout")
public ResponseEntity logout(HttpSession session){
session.removeAttribute("current_user");
return ResponseEntity.ok("已退出登录");
}
}
service
@Service
public class AuthService {
@Resource
private AuthMapper authMapper;
//查询用户
public User getUser(User user){
User userResult = this.authMapper.getUser(user);
return userResult;
}
}
mapper模拟数据库
@Component
public class AuthMapper {
//模拟数据库
List userDataBase = Arrays.asList(new User("1","admin","123456",null,null));
public User getUser(User user){
List list = userDataBase.stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
.collect(Collectors.toList());
return list.size() > 0 ? list.get(0) : null;
}
}
简单界面
resources/templates新增界面
登录界面
登录页面
登录成功界面
登录成功
登录成功
登录失败界面
失败
账号或者密码失败
到此可以启动项目,输入账号密码,界面正常跳转,到此步认证完成
修改登录成功后的界面
登录成功
登录成功
当前用户:
获取苹果:
获取香蕉:
退出登录
新增两个资源获取接口
@RestController
@RequestMapping("/apple")
public class AppleController {
@GetMapping
public String getApple(){
return "资源1苹果";
}
}
@RestController
@RequestMapping("/banana")
public class BnanaController {
@GetMapping
public String getBanana(){
return "资源2香蕉";
}
}
启动项目,登录后,界面会去请求对应的接口获得数据
但此时接口均为做权限的限制,故通过定义适配器+拦截器进行权限的拦截
完善模拟数据库的权限数据
登录成功后,将用户及其权限信息存入session
创建苹果,香蕉资源,再创建管理员角色(苹果,香蕉资源)和苹果经销商角色(苹果资源),
再创建管理员用户(管理员角色(苹果,香蕉资源)),只卖苹果商家用户(苹果经销商角色(苹果资源))
@Component
public class AuthMapper {
public User getUser(User user){
List list = this.getUserList().stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
.collect(Collectors.toList());
return list.size() > 0 ? list.get(0) : null;
}
//模拟用户数据库
public List getUserList(){
List users = new ArrayList<>();
//两个资源呢数据
Resource appleResource = new Resource("1","apple","1");
Resource bananaResource = new Resource("2","banana","1");
//创建三种角色 管理员 苹果卖家
//管理员角色具有苹果 香蕉数据查看权限
Role adminRole = new Role("1","admin",Arrays.asList(appleResource,bananaResource));
//苹果卖家只能看到苹果数据的权限
Role appleRole = new Role("2","appleRole",Arrays.asList(appleResource));
//创建两个用户 超级管理员具有admin角色
User admin = new User("1","admin","123456",Arrays.asList(adminRole));
users.add(admin);
//苹果用户 具有 appleRole角色
User appleOnly = new User("1","apple","123456",Arrays.asList(adminRole));
users.add(appleOnly);
return users;
}
}
创建应用上下文配置MyWebAppConfigurer
(在此处可配置接口的拦截器)
@Component
public class MyWebAppConfigurer implements WebMvcConfigurer {
@Resource
private AuthInterceptor authInterceptor;
//启动界面的简单配置
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//重定向至主页
registry.addViewController("/").setViewName("redirect:/index.html");
}
//配置权限拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/**");
}
}
创建拦截器
对登录状态及资源的范问权限进行拦截
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//设置不需要登录即刻访问的接口
String url = request.getRequestURI();
if (url.contains(".") || url.startsWith("/auth/")){
return true;
}
//未登录的用户
if(null == request.getSession().getAttribute("current_user")){
response.setCharacterEncoding("UTF-8");
response.setHeader("content-type","text/html;charset=UTF-8");
response.getWriter().write("请先登录!");
return false;
}else {
//已登录的用户 进行资源的权限校验
User user = (User)request.getSession().getAttribute("current_user");
if(url.startsWith("/apple") && this.hasPermission("apple",user.getRoles())){
return true;
}else if (url.startsWith("/banana") && this.hasPermission("banana",user.getRoles())){
return true;
}else {
response.setCharacterEncoding("UTF-8");
response.setHeader("content-type","text/html;charset=UTF-8");
response.getWriter().write("暂无权限");
return false;
}
}
}
public boolean hasPermission(String type,List roles){
for (Role r : roles){
if(r.getResources().stream().filter(e -> e.getResourceName().equals(type)).count() > 0){
return true;
}
}
return false;
}
}
至此将对没登录,登陆后不同用户根据权限进行接口访问限制
admin登录
apple登录
未登录访问http://127.0.0.1:5555/banana
至此一个简单的基于RBAC模型的例子完成,下一步结合Spring Security来优化完善。
pom
创建项目,新增依赖
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.projectlombok
lombok
true
yml
Spring Security不需要配置即可直接启动
server:
port: 5556
spring:
application:
name: spring-security-demo
thymeleaf:
prefix: classpath:templates/
suffix: .html
启动类加上@EnableWebSecurity
controller
同上一个例子一样的苹果,香蕉资源
@RestController
@RequestMapping("/apple")
public class AppleController {
@GetMapping
public String getApple(){
return "资源1苹果";
}
}
@RestController
@RequestMapping("/banana")
public class BnanaController {
@GetMapping
public String getBanana(){
return "资源2香蕉";
}
}
此时启动项目,访问这两个接口,均跳转Spring Security自带的登录界面,输入user,密码为控制台日志打印的【Using generated security password】即可登录
修改建应用上下文配置,创建默认用户及权限
通过自定义注入UserDetailsService 用于管理系统的用户账号密码信息,如果不注入,springt security将默认注入一个包含登录名为user的用户(如上),密码打印在控制台
PasswordEncoder密码解析器
spring security要求容器内需要拥有一个PasswordEncoder实例
常用BCryptPasswordEncoder实现类,同一个字符串每次加密得到不同结果
构成:
$2a表示版本 $10 表示算法强度 $xxx 随机盐 xxxx 文本hash值
60位字符串
@Configuration
public class MyWebAppConfigurer implements WebMvcConfigurer {
//启动界面的简单配置
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//此处修改为重定向至 /login接口 /login由spring security提供
registry.addViewController("/").setViewName("redirect:/login");
}
//免密解析器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);
}
/**
* 设置初始用户来源
* 自行注入一个UserDetailsService UserDetailsServiceAutoConfiguration中有默认的
* InMemoryUserDetailsManager
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager =new InMemoryUserDetailsManager(
User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("apple","salary","ROLE_stu").build(),
User.withUsername("apple").password(passwordEncoder().encode("apple")). authorities("apple").build(),
User.withUsername("banana").password(passwordEncoder().encode("banana")).authorities("banana").build());
return manager;
}
}
其中,在设置权限时候,用“ROLE_” + 角色表示角色权限,此时ROLE_不能省略
如配置了角色ROLE_stu,在spring security配置文件中,设置角色权限
.antMatchers("/test/**").hasRole("stu")即可,此时也不能带上"ROLE_"
配置WebSecurityConfig配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义安全拦截策略
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity.csrf().disable() //关闭csrf跨站攻击防御
.authorizeRequests()
.antMatchers("/apple/**").hasAuthority("apple") // 配置拦截路径及资源权限
.antMatchers("/banana/**").hasAuthority("banana")
.antMatchers("/auth/**").permitAll() //登录相关接口不进行拦截
.anyRequest().authenticated() //其他请求都需要登录
.and()
.formLogin().defaultSuccessUrl("/auth/success").failureForwardUrl("/auth/failed"); //登录成功/失败跳转的页面
}
}
修改登录controller
@Controller
@RequestMapping("/auth")
public class LoginController {
//登录成功失败界面
@GetMapping("/success")
public String success(){
return "success";
}
@PostMapping("/failed")
public String failed(){
return "failed";
}
//首页
@RequestMapping("/index")
public String index(){
return "index";
}
//获取当前用户
@PostMapping("/getCurrUser")
@ResponseBody
public Object getCurrentUser(HttpSession session){
return session.getAttribute("current_user");
}
//登出
@PostMapping("logout")
@ResponseBody
public ResponseEntity logout(HttpSession session){
session.removeAttribute("current_user");
return ResponseEntity.ok("已退出登录");
}
/**
* 获取当前用户的多种方式
* @param principal
* @return
*/
@GetMapping("/getUserByPrincipal")
public String getUserByPrincipal(Principal principal){
return principal.getName();
}
@GetMapping(value = "/getLoginUserByAuthentication")
public String currentUserName(Authentication authentication) {
return authentication.getName();
}
@GetMapping(value = "/username")
public String currentUserNameSimple(HttpServletRequest request) {
Principal principal = request.getUserPrincipal(); return principal.getName();
}
// @GetMapping("/getLoginUser")
// public String getLoginUser(){
// User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal() ;
// return user.getUsername();
// }
}
将首页/登录成功页/失败页复制过来
修改首页中获取当前用户部分
function currentUser() {
$.ajax({
type : "GET",
url : "/auth/getUserByPrincipal",
data : {},
success : function (result) {
result = JSON.stringify(result);
$('#currentUser').html("success:" + result);
},
error : function (error) {
$('#currentUser').html("error:" + error);
}
})
}
至此简单的spring security项目准备完毕,但是此时用户信息及权限规则,均通过UserDetailsService写至内存中,实际项目需要结合数据库来获取用户权限数据
启动项目-》登录前,直接请求/banana接口,发现均会跳转至自带登录界面,用banana/banana登录后,正常获取banana资源,而获取不到apple资源
直接访问http://127.0.0.1:5556/banana
正常获取数据,访问http://127.0.0.1:5556/apple
返回403无法访问
使用自带/logout接口退出当前用户后,访问/banana接口,则此时跳转至登录界面
SpringSecurity通过引用Spring容器的UserDetailsService来管理用户数据,默认情况下会提供一个为user的用户,可通过自定义注入改变用户数据,但实际开发中,用户数据及权限配置信息来源于数据库。在SpringSecurity提供JdbcUserDetailsManager对数据库的用户数据进行管理
自定义登录界面整合数据库权限管理
修改配置从数据库获取用户及权限
修改UserDetails,从数据库加载用户信息
修改WebSecurityConfig中httpSecurity,改成从数据库加载授权配置
SpringSecurity提供多种密码解析器,CryptPassEncoder/Argon2PasswordEncoder等,接实现PassEncoder接口,最常见为BCryptPasswordEncoder
自定义拦截配置
配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,通过重写的configure(HttpSecurity httpSecurity)进行拦截规则配置,包括访问控制,登录登出界面设置等
自定义登录
通过配置HttpSecurity中的方法来自定义
loginPage()配置登录页面
如在WebSecurityConfig中新增.loginPage("/auth/index")
当输入http://127.0.0.1:5556/login
页面跳转为自定义界面 http://127.0.0.1:5556/auth/index
此时需要修改登录界面index.html 及登录请求接口的逻辑
修改index.html中action 编写/auth/tologin接口用于处理登录逻辑,自定义后,将不会使用UserDetailsService来进行处理
loginProcessingUrl() 设置登录逻辑
登录时提交remember-me参数 on/yes/1/true 则会记住当前用户的token至cookie中(默认失效时间2周.tokenValiditySeconds(60)设置失效时间)
httpSecurity.rememberMe().rememberMeParameter("remember-me") //登录时,前端传参的名称
httpSecurity..authorizeRequests().antMachers()设置请求路径匹配
antMachers().permitAll()允许所有人访问,
antMachers().denyAll()所有人拒绝访问,
antMachers().anonymous()未登录可以访问,已登录不可访问
antMachers().hasAuthority()/hasRole() 配置需要有对应的权限/角色才能访问
Cross-Site Request Forgery 跨站点请求伪造,一种安全攻击手段,利用客户端信息伪装成正常用户进行攻击。
当打开csrf配置后,SpringSecurity提供CsrfFilter对csrf参数进行检查,每次请求,session中加入_csrf的token,每次带上token,SpringSecurity进行检查
@ControllerAdvice注入一个异常处
理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。
权限控制注解
以下四个注解默认不生效,需要在配置类(MyWebAppConfigurer)或者启动类加上@EnableGlobalMethodSecurity(prePostEnabled = true)开启
@PreAuthorize("hashRole(‘admin’)") 执行方法前判断是都具有该角色权限
注解支持ROLE_admin/admin均可,但是配置类中.antMatchers("/test/**").hasRole("stu")不能带上"ROLE_"
@PostAuthorize("returnObject.name == authentication.name") 方法执行后,判断返回的值是否与认证主体中的某个值相等,若相等则返回,否则抛出异常
@PreFilter(filterTarget=”a“, value=”filterObject%2==0“) 方法执行前,过滤参数a 当a%2不等于0则会被过滤,不会传入方法中(入参a 为List
a) @PostFilter("filterObject.name == authentication.name“) 方法执行后根据条件过滤结果
角色控制注解
@EnableGlobalMethodSecurity(securedEnable = true)开启。
@Secured("ROLE_stu") 作用于方法,类,判断是否具备该角色(角色严格区分大小写)
通过认证后,SpringSecurity提供会话管理,保存用户的认证信息,以避免每次操作都要进行认证操作。认证通过后,身份信息将放入SecurityContextHolder上下文,SecurityContext与当前线程绑定,用于方便获取用户身份
通过SecurityContextHolder.getContext().getAuthentication()获取当前用户登录信息
/**
* 获取当前用户的多种方式
* @param principal
* @return
*/
@GetMapping("/getUserByPrincipal")
@ResponseBody
public String getUserByPrincipal(Principal principal){
return principal.getName();
}
@GetMapping(value = "/getLoginUserByAuthentication")
@ResponseBody
public String currentUserName(Authentication authentication) {
return authentication.getName();
}
@GetMapping(value = "/username")
@ResponseBody
public String currentUserNameSimple(HttpServletRequest request) {
Principal principal = request.getUserPrincipal(); return principal.getName();
}
@GetMapping(value = "/username")
@ResponseBody
public String getUserByContext() {
Principal principal = (Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal.getName();
}
通过配置sessionCreationPolicy参数来管理Session
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义安全拦截策略
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
......
策略包含
always 如果无Session则创建一个
if required 如果需要登录时创建一个(默认)
never SpringSecurity不会创建Session,如果应用其他地方创建,则会被使用
stateless SpringSecurity绝不创建也不使用Session
在yml文件中直接设置session过期时间
server.servlet.session.timeout = 3000s (springsecurity默认一分钟 小于一分钟也会按照一分钟失效处理)
session过期后可通过SpringSecurity设置跳转界面
httpSecurity . sessionManagement (). expiredUrl ( "/auth/index ?error=EXPIRED_SESSION" ) //session过期后跳转. invalidSessionUrl ( "/auth/index ?error=INVALID_SESSION" ); //传入无效sessionId跳转
同账号多端登录前者被踢下线功能
httpSecurity.sessionManagement()
.maximumSessions(1) //设置某段时间内允许几个端登录
.maxSessionsPreventsLogin(false) //true表示已经登录则不允许再次登录 false允许再次登录但前者会被踢下线
.expiredSessionStrategy(new CustomExpiredSessionStrategy()) //自定义被踢下线后的操作 需要实现自定义策略
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy{
private ObjectMapper objectMapper = new ObjectMapper();
@override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event){
Map
map = new HashMap(); map.put("code",403);
map.put("msg","您已在其他地方登录,被迫下线" + event.getSessionInformation().getLsatRequest());
String json = objectMapper.writeValueAsString(map);
event.getResponse().setContentType("application/json;charset=utf-8");
event.getResponse().getWriter().write(json);
}
}
通过两个不同的浏览器进行同账号登录测试
在yml文件中直接设置
server.servlet.session.cookie.http ‐ only = true # true 浏览器脚本无法访问cookieserver.servlet.session.cookie.secure = true # true cookie仅通过Https发送
SpringSecurtity提供退出接口/logout 跳转至登出界面,可以直接调用
也可在WebSecurityConfig中自定义退出界面/退出后跳转地址
.and()
.logout() //开启自定义退出,使用WebSecurityConfigurerAdapter 会自动被应用 .logoutUrl("/logout") //默认退出地址
.logoutSuccessUrl("/auth/logout") //退出后的跳转地址
.addLogoutHandler(new SecurityContextLogoutHandler()) //添加LogoutHandler,负责退出时的清理工作.默认 SecurityContextLogoutHandler会被添加为最后一个LogoutHandler
.invalidateHttpSession(true); //指定是否在退出时让HttpSession失效,默认是true
退出时,会进行的操作(SecurityContextLogoutHandler)
1.http session失效
2.清除SecurityContext上下文
3.跳转至退出后的地址
SecurityContextLogoutHandler退出时负责SecurityContext的清理工作
单体系统演变至多服务,多个服务利用一套独立的第三方系统提供统一的认证授权
统一认证授权
独立的认证服务,用于统一处理认证授权,不管是不同类型的用户,不同类型的客户端(web,h5,小程序,app等),采用一致的认证授权会话处理机制,实现统一登录认证授权
需要支持多种认证方式:账号密码,短信验证,二维码,人脸识别等灵活切换
多重认证场景
购物,支付需要不同安全等级需要对应认证场景
应用接入认证
开放部分API给第三方使用。内部与外部第三方采用统一的
认证方案(基于session与token两种方案)
方案1基于session的认证方式
在分布式中,将session信息同步至各个服务,并对请求进行负载均衡
做法:
1.进行session复制:多个服务器之间同步session,使得session保持一致,但是对外透明
2.session黏贴:用户访问服务器集群中某台服务器后,后续所有请求都需要落到该服务器上
3.session集中存储:将session存入分布式缓存中,所有服务器均从分布式缓存中获取session信息
优点:更好的在服务端进行会话控制,安全性较高
缺点:但是客户端需要存储sessionId,对不同的客户端不能有效使用等
方案2基于token的认证方式
生成认证的token存储,每次请求携带并进行校验
选择方案
统一认证服务(UAA)和网关结合来进行认证授权
uaa负责接入方认证,登录用户认证,授权,令牌管理,完成实际的用户认证授权
API网关作为整个分布式系统的统一入口,进行身份认证,监控,负载均衡等
OAuth开放授权标准,允许用户授权第三方应用B获取在微信上注册的信息,从而不需要向B提供微信账号密码,避免B获取用户在微信上的所有数据内容。OAuth协议用于保证双方的可信。
用户在B上选择通过微信登录-》弹出微信登录方式(账号密码/二维码等)-》选择弹出的微信同意登录-》B获取用户微信账号信息-》B新建账号与微信绑定-》用B账号进行登录
一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求
客户端Client:如浏览器,微信客户端,不存储资源,需要通过资源拥有者授权去请求资源服务器的资源
资源拥有者Resource Owner:用户,也可以是应用程序,表示某个资源的拥有者
授权服务器Authorization Server:认证服务器,如微信,服务提供者对资管拥有者的身份认证,对访问资源进行授权,认证成功给客户端发放令牌(access_token)作为凭证
资源服务器Resource Server:如微信服务器,B服务器,通过OAuth协议让B获取用户在微信上的用户信息,B同时通过OAuth协议让用户访问自己的资源
clientDetails:cilent_id客户信息,代表B在微信中的唯一索引secret :密钥,B获取微信的信息需要提供加密字段scope:授权作用域,代表B获取微信的信息访问access_token:授权码,B获取微信用户信息的凭证,如微信的接口调用凭证grant_type:授权类型,如微信支持基于授权码的authorization_code模式,OAuth提供多种授权方式userDetails:授权用户标识user_id,如微用户在微信中微信号
AuthorizationEndpoint 服务用于认证请求。默认URL:/oauth/authorizeTokenEndpoint 服务用于访问令牌的请求。默认URL:/oauth/tokenOAuth2AuthenticationProcessingFilter 用于对请求方给出的身份令牌进行解析鉴权
1.客户请求server-uaa服务申请access_token授权码
2.客户携带access_token访问server-b服务
3.server-b校验access_token合法性,若合法返回资源信息
描述:通过码云实现模拟第三方登录(授权码模式)
1.新建简单项目,创建个简单回调页面(客户端)
2.在码云上注册应用,获取Client ID(客户端id),Client Secret(密钥)等(授权服务器,码云同为资源服务器)
上面说过一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求,所以此处创建第三应用可以理解为,在码云上登记一个客户端信息
3.点击模拟请求进行测试-》同意授权
4.页面跳转至我们设置的回调界面,在链接上带回一个code
5.查看码云OAuth文档
Gitee OAuth 文档https://gitee.com/api/v5/oauth_doc
请求的几个参数
1.clientDetails(client_id) 客户信息,第三方应用id,项目在码云中的唯一索引
2.secret 密钥,代表获取码云信息需要提供的加密字段(与码云加密算法有关)
3.scope 授权作用域,可以获取码云信息的范围
4.access_token 授权码,允许获取码云信息的凭证
5.grant_type 四种授权类型
1.通过请求,进行授权模拟(授权码模式)
.根据文档,通过浏览器将用户引导至码云第三方认证页面(GET)
https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
带入实际参数client_id,redirect_uri,response_type
https://gitee.com/oauth/authorize?client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&response_type=code
2.出现第3步,第四步相同操作
3. 将授权码code向码云认证服务器发送POST请求,获取access_token(有效期一天)
https://gitee.com/oauth/token?grant_type=authorization_code&code=c59e19acf5eb78211ca4246d21cd652cbba0e3124d86c7d9ea4deeecd1e9d513&client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&client_secret=9548b96604a5ab1a50b201c09065a2e0c84e95be3c4163704ee29341080afcd5
当access_token失效后,可以通过refresh_token重新获取access_token(POST)
https://gitee.com/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}
1.客户端:例子中创建的客户端服务,本身无资源,通过浏览器去获取码云资源,及需要通过资源拥有者的授权去资源服务器获取资源
2.资源拥有者,例子中拥有码云账号的用户,资源拥有者
3.授权服务器(认证服务器),例子中码云认证服务器,对资源拥有者身份认证授权,认证成功发放令牌(access_token)作为客户端访问资源服务器凭证
4.资源服务器,例子中码云,存储资源的云服务器,通过OAuth协议,用户可以获取码云账户数据,仓库数据,代码等资源
用户访问客户端,客户端向授权服务器发起授权
授权服务器,引导用户进入授权界面,等待用户同意授权
用户同意授权
回调客户端界面,带回授权码
客户端通过授权码向授权服务器请求获取令牌access_token
适用:安全性最高,最常用的流程
授权码模式简化
与授权码模式相似,但是用户同意授权后直接返回access_token
适用:纯前端项目
客户端与资源所有者高度可信情况下可用,如客户端为系统的一部分
资源拥有者直接提供账号密码给客户端,向授权服务器申请令牌access_token
适用:高度信任的情况下
该情况适用于无前端的纯后台项目,客户端向授权服务器发送身份信息,请求access_token
适用:纯后端项目
本例资源服务,认证服务均为同一个服务,采用客户端模式(client_credentials)
server:
port: 8888
spring:
application:
name: oauth2-demo
引入安全认证依赖,引入后,springboot便会对资源服务器上所有资源进行默认的保护
org.springframework.security.oauth spring-security-oauth2 2.3.6.RELEASE org.springframework.boot spring-boot-starter-security
1.token endpoint安全束缚配置
主要配置允许客户端以Form表单形式登录/配置密码加密方式等)
2.客户端详情设置
客户端详情包括(client_id,client_secret,grant_type,scope)
剋设置客户端详情存储位置,内存或者数据库
3.配置授权,token endpoint,令牌服务
本例采用客户端模式
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
//配置采用的加密方式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//配置安全约束 定义令牌终结点上的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许客户端以form表单登录,如微信获取access token
security.allowFormAuthenticationForClients();
}
//配置客户端详细信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//client_id
.withClient("client")
//授权方式 "authorization_code", "password", "client_credentials", "implicit", "refresh_token"
.authorizedGrantTypes("client_credentials")
// 授权范围 all表示所有,write等
.scopes("all")
// client_secret配置加密类型,如果不需要 则直接 .secret("{noop}123456"); {noop表示空操作}
.secret(new BCryptPasswordEncoder().encode("123456"));
}
//令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
}
}
1.资源服务器安全配置
2.http安全配置,保护资源API
配置类
@Configuration
@EnableResourceServer
public class MyResourceServerConfigurer extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/apple/**").authenticated();
}
}
资源请求API
@RestController
@RequestMapping("/apple")
public class AppleController {
@GetMapping
public String getApple(){
return "资源1苹果";
}
}
至此简单的认证服务与资源服务配置完成,本例认证服务与资源服务为同一个项目
未授权进行资源的请求,此时报错,提示未授权
获取token
SpringBoot OAuth 默认获取token的endpoint路劲为 /oauth/token
其中expires_in 为失效时间(秒),每次重复请求,返回同一个token,但是失效时间再减少
带上token进行请求,得到结果
目的:将客户端Client信息与token存储至数据库(token一般存放redis)
本例中用到postgre sql
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY, -- 客户端id
resource_ids VARCHAR(256) , -- 资源id集合,英文逗号隔开
client_secret VARCHAR(256) , -- 客户端密钥
scope VARCHAR(256) , -- 授权范围
authorized_grant_types VARCHAR(256) , -- 授权类型 authorization_code,password,refresh_token,implicit,client_credentials,英文逗号隔开
web_server_redirect_uri VARCHAR(256), -- 客户端重定向uri
authorities VARCHAR(256), -- 客户端拥有spring security权限值
access_token_validity INTEGER , -- token有效时间 秒 默认12小时
refresh_token_validity INTEGER , -- refresh_token有效时间 默认30天
additional_information VARCHAR(4096) , -- 预留json字段
autoapprove VARCHAR(256) -- 是否启动自动approve操作,适用于授权码模式authorization_code
);
create table oauth_access_token (
token_id VARCHAR(256) , -- MD5算法加密后的access_token
token bytea, -- access_token序列化二进制数据格式 mysql中为BLOB类型
authentication_id VARCHAR(256) PRIMARY KEY , -- 根据username,client_id,scopeMD5加密生成的主键
user_name VARCHAR(256), -- 用户名
client_id VARCHAR(256), -- 客户端id
authentication bytea, -- OAuth2Authentication对象序列化后的二进制数
refresh_token VARCHAR(256) -- refresh_token MD5加密后的值
);
官方完整建表sql
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
注意此处密码应该进行BCrypt加密
123456 对应
$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu
INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('client', 'apple', '$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu', 'all', 'client_credentials', null, null, null, null, null, null);
pom文件新增对应数据库的支持依赖
org.postgresql postgresql runtime
yml
server:
port: 8888
spring:
application:
name: oauth2-demo
datasource:
# 数据库引擎
driver-class-name: org.postgresql.Driver
# 数据库地址 characterEncoding防止出现中文乱码 若为https则无需加useSSL
url: jdbc:postgresql://127.0.0.1:5432/test?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&yearIsDateType=false&stringtype=unspecified
# 用户
username: postgres
# 密码
password: 123456
# 数据库连接类型
# 时区
jackson:
time-zone: GMT+8
给资源配置resourceId
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception
//super.configure(resources);
resources.resourceId("apple");
}
从原来从内存中获取客户端信息改为从数据库获得客户端信息
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
public MyAuthorizationConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
//配置采用的加密方式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
//配置安全约束 定义令牌终结点上的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许客户端以form表单登录,如微信获取access token
security.allowFormAuthenticationForClients();
}
//令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new JdbcTokenStore(dataSource));
}
}
请求测试
每次请求,OAuth会去客户端信息表oauth_client_details查询当前发送过来的客户信息是否正确,认证成功后将返回令牌access_token,同时由于设置了将令牌存储至数据库,此时oauth_access_token也会新增一条数据
postman认证成功返回令牌
成功获取资源服务的资源
至此OAuth2 客户端授权模式结合数据库存储的简单例子结束,本例中创建了两张需要的表,一为客户端详情表,代替之前在代码中将客户端信息写入内存的方式,二是令牌token表,用于存储token,但实际开发中,token一般不存储在数据库中,不然每次请求接口都需要访问数据库
上面的例子已经设计将token存在内存,数据库中,但更合理的应该存在redis上,且redis可设置数据的有效期。这样可以避免高并发情况下频繁访问数据库。在分布式架构中,将token存在其中一台服务器实例的内存中也不合适。
pom新增redis依赖
org.springframework.boot
spring-boot-starter-data-redis
yml配置文件新增redis配置
redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 本地localhost/127.0.0.1 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # 连接超时时长(毫秒) timeout: 6000 # Redis服务器连接密码(默认为空) password:
认证服务MyAuthorizationConfig修改
令牌访问端点修改为redis存储方式
@Resource
private RedisConnectionFactory redisConnectionFactory;
......
//令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory));
//endpoints.tokenStore(new JdbcTokenStore(dataSource));
}
启动本地redis,启动服务请求获得token,此时redis中
JWT简单介绍
JSON Web Token(JWT),客户端服务器通过JWT规定格式进行身份认证完成交互,JWT分三段,每段通过.隔开,包含不同信息分别为
Header头部:JSON数据Base64编码,包含加密类型等
PayLoad负载:JSON数据Base64编码,包含客户端授权信息
Signature签名:头部+负载+密钥(盐)通过加密算法获得,防止数据篡改(盐存在服务端,保密)
导入依赖
org.springframework.security
spring-security-jwt
1.1.1.RELEASE
修改认证服务配置
//设置密钥串
private static final String SIGNING_KEY = "oauth_test";
......
//令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
//设置token转换器
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置加签密钥
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
//设置校验器
jwtAccessTokenConverter.setVerifier(new MacSigner(SIGNING_KEY));
TokenStore tokenStore = new JwtTokenStore(jwtAccessTokenConverter);
endpoints.accessTokenConverter(jwtAccessTokenConverter);
endpoints.tokenStore(tokenStore);
}
启动服务请求获得token,并成功请求资源接口
此token通过官网解码JSON Web Tokens - jwt.ioJSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).https://jwt.io/