简介
上一节Springsecurity 用户认证
 Springsecurity 拥有强大的认证和授权功能并且非常灵活,,一来说我们都i有以下需求
 可以帮助应用程序实现以下两种常见的授权需求:
-  
用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表
 -  
用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息
 
RBAC权限模型
 RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型 即每个用户对应不同角色,每个角色对应不同功能
用户-角色-权限-资源
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例:
- 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。
 
| 列名 | 数据类型 | 描述 | 
|---|---|---|
| user_id | int | 用户ID | 
| username | varchar | 用户名 | 
| password | varchar | 密码 | 
| varchar | 电子邮件地址 | |
| … | … | … | 
- 角色表(Role table):存储所有可能的角色及其描述。
 
| 列名 | 数据类型 | 描述 | 
|---|---|---|
| role_id | int | 角色ID | 
| role_name | varchar | 角色名称 | 
| description | varchar | 角色描述 | 
| … | … | … | 
- 权限表(Permission table):定义系统中所有可能的权限。
 
| 列名 | 数据类型 | 描述 | 
|---|---|---|
| permission_id | int | 权限ID | 
| permission_name | varchar | 权限名称 | 
| description | varchar | 权限描述 | 
| … | … | … | 
- 用户角色关联表(User-Role table):将用户与角色关联起来。
 
| 列名 | 数据类型 | 描述 | 
|---|---|---|
| user_role_id | int | 用户角色关联ID | 
| user_id | int | 用户ID | 
| role_id | int | 角色ID | 
| … | … | … | 
- 角色权限关联表(Role-Permission table):将角色与权限关联起来。
 
| 列名 | 数据类型 | 描述 | 
|---|---|---|
| role_permission_id | int | 角色权限关联ID | 
| role_id | int | 角色ID | 
| permission_id | int | 权限ID | 
| … | … | … | 
在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。
当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置。其实就是权限或者角色不会直接在用户属性里,而是在多表联查中赋予
 比如
 来获取用户权限或者角色
<select id="selectPermsByUserId" resultType="java.lang.String">
    SELECT
        DISTINCT m.`perms`
    FROM
        sys_user_role ur
        LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
        LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
        LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
    WHERE
        user_id = #{userid}
        AND r.`status` = 0
        AND m.`status` = 0
</select>
 
基于request的授权
在首页的用户笔记中有项目目录,现在修改其中的俩个接口权限
官方架构
 
之前只要携带正确的token 就可以访问
 
 修改配置文件后
  @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
                        .requestMatchers("/user/list").hasAuthority("user:list")
                        
                        .requestMatchers("/user/add").hasAuthority("user:add")
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
//                        .requestMatchers("**").permitAll()
                        .anyRequest().authenticated())
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
            ;
        return http.build();
    }
 
返回的内容是自己定义的json
 
 但是响应码确实变成了403 security不允许访问
 
实现授权:权限-资源
实现不同权限访问不同接口
配置文件限定接口
@EnableMethodSecurity
@Configuration
@AllArgsConstructor
public class WebSecurityConfig {
    private final ApplicationEventPublisher applicationEventPublisher;
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private JwtUtil jwtUtil;
//    从配置文件注入  ioc先扫描配置文件
@Resource
    UserMapper userMapper;
    /**
     * 这个Bean创建了一个认证管理器对象,它是Spring Security认证的核心组件之一。
     * 认证管理器负责协调和管理认证流程,并委托给一个或多个认证提供者(在这里,使用了daoAuthenticationProvider)来进行具体的认证操作。
     * 这里通过创建一个ProviderManager对象,将之前配置的daoAuthenticationProvider添加到认证管理器中。
     * 还通过setAuthenticationEventPublisher()方法设置了一个事件发布器,用于在认证事件发生时发布相关的事件,
     * 这里使用了DefaultAuthenticationEventPublisher,并传入了一个applicationEventPublisher对象,可能用于发布认证事件到Spring的事件机制中。
     * @return
     */
    @Bean
    public AuthenticationManager authenticationManager() {
        List<AuthenticationProvider> providerList = new ArrayList<>();
        providerList.add(daoAuthenticationProvider());
        ProviderManager providerManager = new ProviderManager(providerList);
        providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));
        return providerManager;
    }
    /**
     * 是Spring Security用于处理基于数据库的用户认证的提供者。
     * DaoAuthenticationProvider需要一个UserDetailsService对象来获取用户的详细信息进行认证,
     * 所以通过setUserDetailsService()方法设置了我们之前设置的manager。
     * @return
     */
    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder( passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(new DBUserDetailsManager(userMapper));
        return daoAuthenticationProvider;
    }
    /**
     * 把默认的密码加密器换成我们自定义的加密器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
      return new BCryptPasswordEncoder();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
                        .requestMatchers("/user/list").hasAuthority("user:list")//对接口定制限定权限
                        .requestMatchers("/user/add").hasAuthority("user:add")
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
//                        .requestMatchers("**").permitAll()
                        //对所有请求开启授权保护
                        .anyRequest()
                        //已认证的请求会被自动授权
                        .authenticated()
                       )
                .exceptionHandling(
            //   异常结果处理 1.认证异常处理2.授权异常处理
					exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint())
                        .accessDeniedHandler((request, response, e)->{
                            //创建结果对象
                            HashMap result = new HashMap();
                            result.put("code", -1);
                            result.put("message", "没有权限");
                            //转换成json字符串
                            String json = JSON.toJSONString(result);
                            //返回响应
                            response.setContentType("application/json;charset=UTF-8");
                            response.getWriter().println(json);
                        })
                )
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
            ;
        return http.build();
    }
}
 
规范用户类
改造用户类
 这里做简单模拟 写一个字段作为权限字段,往往实际开发中是一个json 数组用于转换
/**
 * @TableName user
 */
@TableName(value ="user")
@Data
public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;
    private Integer enabled;
    //实际开发中权限字段多半是json 里卖装的权限数组
    @TableField(exist = false)
    private List<GrantedAuthority> authorities;
    private static final long serialVersionUID = 1L;
}
 
security定义的规范用户类
@Data
public class UserDetail implements UserDetails {
    private User user;
    //存储SpringSecurity所需要的权限信息的集合 构造函数时候用于将user的json数组转换为这个权限对象
    private List<GrantedAuthority> authorities;
    @Override
    public List<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    @Override
    public String getUsername() {
        return user.getUsername();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}
 
userdetailsService的实现类
@Component
@Slf4j
@AllArgsConstructor
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    private UserMapper userMapper;
//    这样就可以按照security的规范来使用用户的管理
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
    @Override
    public void createUser(UserDetails userDetails) {
//        在sql中插入信息
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        user.setEnabled(1);
        userMapper.insert(user);
    }
    @Override
    public void updateUser(UserDetails user) {
    }
    @Override
    public void deleteUser(String username) {
    }
    @Override
    public void changePassword(String oldPassword, String newPassword) {
    }
    @Override
    public boolean userExists(String username) {
        return false;
    }
    //security底层会根据这个方法来对比用户
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        这里用户账户是唯一的
    User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username));
       if (user == null){
           throw new UsernameNotFoundException("系统用户不存在");
       }else{
//           1表示可用
           boolean isenabled = user.getEnabled() == 1;
/**
 * ,任何非零的整数值都会被视为 true,而 0 会被视为 false。
 */
log.info("数据库个根据用户名获取用户"+user);
        //模拟系统权限列表
          List<GrantedAuthority> authorities = new ArrayList<>();
  // 写一个静态数据模拟用户数据库中的权限
           authorities.add(()->"user:list");
           user.setAuthorities(authorities);
           UserDetail detail = new UserDetail();
           detail.setUser(user);
           detail.setAuthorities(authorities);
           return detail;
       }
    }
}
 
改造过滤器
@Slf4j
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private final RedisCache redisCache;
    private final   JwtUtil jwtUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = request.getHeader("token");
            if (tokenNotRequired(request.getRequestURI())) {
//                    登录接口直接放行
                filterChain.doFilter(request, response);
                return;
            }
            if (token == null || token.trim().isEmpty()) {
                throw new AuthenticationException("需要登录") {};
            }
            String username = jwtUtil.getUsernameFromToken(token);
            String redisKey = "logintoken:" + username;
            String jsonString = redisCache.getCacheObject(redisKey);
            if (jsonString == null || jsonString.trim().isEmpty()) {
                throw new AuthenticationException("用户登录已过期") {};
            }
            UserDetail userInfo = JSON.parseObject(jsonString, UserDetail.class);
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken); //设置给上下文对象
            filterChain.doFilter(request, response);
        }
        //对所有抛出的异常进行处理
        catch (AuthenticationException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(JSON.toJSONString(Result.nologin(e.getMessage())));
        }
    }
    private boolean tokenNotRequired(String requestURI) {
        return "/auth/login".equals(requestURI) || "/auth/info".equals(requestURI);
    }
}
 
访问上下文对象
    @GetMapping("/")
    public Result index() {
        SecurityContext context = SecurityContextHolder.getContext();//存储认证对象的上下文
        Authentication authentication = context.getAuthentication();//认证对象
        String username = authentication.getName();//用户名
        Object principal =authentication.getPrincipal();//身份
        Object credentials = authentication.getCredentials();//凭证(脱敏)
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();//权限
        System.out.println(username);
        System.out.println(principal);
        System.out.println(credentials);
        System.out.println(authorities);
        HashMap<String, Object> map = new HashMap<>();
        map.put("认证对象", authentication);
        map.put("身份信息", principal);
        map.put("creden", credentials);
        return Result.success(map);
    }
 
此时可以发现权限已经设置好了
 
上述的文件操作中,我在loaduserbyusername中只添加了user:list权限,现在再次访问这俩个有权限设置的接口
userl:list 权限
 
user:add权限
 因为没有该权限 所以触发未授权处理器
 
实现用户-角色-资源
修改配置文件接口
     http.csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
                        .requestMatchers("/user/list").hasAuthority("user:list")
                        .requestMatchers("/user/add").hasRole("admin")
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
//                        .requestMatchers("**").permitAll()
                        .anyRequest().authenticated())
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
        ;
        return http.build();
 
该接口
//    动态添加系统授权用户
    @PostMapping("/add")
    public String addUser(@RequestBody User user) {
log.info("add user"+user);
        userService.adduser(user);
return "添加用户";
    }
 
此时一个接口需要user:list权限 (查看用户列表) 一个需要当前用户是管理员角色 才能对系统用户进行新增
修改我们定义的用户
@TableName(value ="user")
@Data
public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;
    private Integer enabled;
    //实际开发中权限字段多半是json 里卖装的权限数组
    @TableField(exist = false)
    private List<GrantedAuthority> authorities;
    //模拟数据存放的都是用户json数组
    @TableField(exist = false)
    public List<String> roles;
    private static final long serialVersionUID = 1L;
}
 
 
修改userdetailsService实现类 静态赋予角色(模拟该用户数据中的角色 这里的用户表只有简单的用户密码和账户字段)
@Component
@Slf4j
@AllArgsConstructor
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    private UserMapper userMapper;
    //    这样就可以按照security的规范来使用用户的管理
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
    @Override
    public void createUser(UserDetails userDetails) {
//        在sql中插入信息
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        user.setEnabled(1);
        userMapper.insert(user);
    }
    @Override
    public void updateUser(UserDetails user) {
    }
    @Override
    public void deleteUser(String username) {
    }
    @Override
    public void changePassword(String oldPassword, String newPassword) {
    }
    @Override
    public boolean userExists(String username) {
        return false;
    }
    //security底层会根据这个方法来对比用户
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        这里用户账户是唯一的
        User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username));
        if (user == null){
            throw new UsernameNotFoundException("系统用户不存在");
        }else{
//           1表示可用
            boolean isenabled = user.getEnabled() == 1;
/**
 * ,任何非零的整数值都会被视为 true,而 0 会被视为 false。
 */
            log.info("数据库个根据用户名获取用户"+user);
            List<String> roles = new ArrayList<>();//模拟数据中的该用户的角色数据
            //假如该用户有俩个数据
            roles.add("admin");
            roles.add("leader");
            //模拟系统权限列表
            List<GrantedAuthority> authorities = new ArrayList<>();
            // 写一个静态数据模拟用户数据库中的权限
            authorities.add(()->"user:list");
            roles.forEach(role-> authorities.add(()->"ROLE_"+role));
            user.setAuthorities(authorities);
            UserDetail detail = new UserDetail(user);
            detail.setAuthorities(authorities);
            return detail;
        }
    }
}
 
关键代码块解析
 这里创建一个集合模拟该用户在数据的角色数据,然后和用户数据的添加设置到userdetals的实现类中,这里为什么这么设置角色?为什么角色和权限放在同一字段
ist<String> roles = new ArrayList<>();//模拟数据中的该用户的角色数据
            //假如该用户有俩个数据
            roles.add("admin");
            roles.add("leader");
            //模拟系统权限列表
            List<GrantedAuthority> authorities = new ArrayList<>();
            // 写一个静态数据模拟用户数据库中的权限
            authorities.add(()->"user:list");
            roles.forEach(role-> authorities.add(()->"ROLE_"+role));
            user.setAuthorities(authorities);
 
打开限制接口的源码hasRole(“admin”)
 
 发现和权限的区别只有多了一个前缀作为识别而已
 
 重启项目访问上下文对象 角色本质底层也是权限,只是有一个前缀醉作为识别
 
访问限制接口
成功
 
已经和认证处理器一样拥有丰富的授权事件处理器

基于方法的授权
这个是偏向主流的鉴权官网地址->方法安全
配置文件类中添加如下注解 开启方法鉴权 这种开启功能的注解一般放在启动类
@EnableMethodSecurity
 
userdetailService接口的实现类逻辑依旧是从数据库获取用户和权限等,所以不做修改依旧使用上面的实现类模拟->userdetailservice实现
将配置文件中有关授权的配置删除
@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
//                        .requestMatchers("**").permitAll()
                        .anyRequest().authenticated())
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
        ;
        return http.build();
    }
 
对于需要限定的接口添加PreAuthorize注解
//  限定角色:  用户必须有 ADMIN 角色 并且 用户名是 admin 才能访问此方法
  @PreAuthorize("hasRole('admin') and authentication.name == 'admim'")
    @GetMapping("/list")
public List<User> getList(){
    return userService.list();
}
//限定权限: 用户必须有 USER_ADD 权限 才能访问此方法
  @PreAuthorize("hasAuthority('user:add')")
@PostMapping("/add")
public void add(@RequestBody User user){
    userService.saveUserDetails(user);
}
 
由于当前用户名是123 所以也会触发授权失败的处理
 
 修改回来即可访问成功
 
outh2
springsecurity官网配置
OAuth 2.0 提供了一种安全的方式,让用户允许第三方应用访问他们在其他网站或服务上的受保护资源,而无需共享他们的用户名和密码。
1、OAuth2简介
1.1、OAuth2是什么
“Auth” 表示 “授权” Authorization
“O” 是 Open 的简称,表示 “开放”
连在一起就表示 “开放授权”,OAuth2是一种开放授权协议。
OAuth2最简向导:The Simplest Guide To OAuth 2.0
1.2、OAuth2的角色
OAuth 2协议包含以下角色:
- 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。
 - 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。
 - 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。
 - 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。
 

1.3、OAuth2的使用场景
开放系统间授权
社交登录
在传统的身份验证中,用户需要提供用户名和密码,还有很多网站登录时,允许使用第三方网站的身份,这称为"第三方登录"。所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

开放API
例如云冲印服务的实现

现代微服务安全
单块应用安全

微服务安全

企业内部应用认证授权
-  
SSO:Single Sign On 单点登录
 -  
IAM:Identity and Access Management 身份识别与访问管理
 
1.4、OAuth2的四种授权模式
RFC6749:
RFC 6749 - The OAuth 2.0 Authorization Framework (ietf.org)
阮一峰:
OAuth 2.0 的四种方式 - 阮一峰的网络日志 (ruanyifeng.com)
四种模式:
- 授权码(authorization-code)
 - 隐藏式(implicit)
 - 密码式(password)
 - 客户端凭证(client credentials)
 
第一种方式:授权码
授权码(authorization code),指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用,最复杂,也是最安全的,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。比如常见的jwt

- 注册客户应用:客户应用如果想要访问资源服务器需要有凭证,需要在授权服务器上注册客户应用。注册后会获取到一个ClientID和ClientSecrets
 

第二种方式:隐藏式
隐藏式(implicit),也叫简化模式,有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。
RFC 6749 规定了这种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为隐藏式。这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
 
https://a.com/callback#token=ACCESS_TOKEN
将访问令牌包含在URL锚点中的好处:锚点在HTTP请求中不会发送到服务器,减少了泄漏令牌的风险。
 
第三种方式:密码式
密码式(Resource Owner Password Credentials):如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌。
 比如一个没有和其他服务对接的应用
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

第四种方式:凭证式
凭证式(client credentials):也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
 
1.5、授权类型的选择

2、Spring中的OAuth2
2.1、相关角色
回顾: OAuth 2中的角色
- 资源所有者(Resource Owner)
 - 客户应用(Client)
 - 资源服务器(Resource Server)
 - 授权服务器(Authorization Server)
 
2.2、Spring中的实现
OAuth2 :: Spring Security
Spring Security
- 客户应用(OAuth2 Client):OAuth2客户端功能中包含OAuth2 Login
 - 资源服务器(OAuth2 Resource Server)
 
Spring
- 授权服务器(Spring Authorization Server):它是在Spring Security之上的一个单独的项目。
 
2.3、相关依赖
<!-- 资源服务器 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- 客户应用 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- 授权服务器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
 
2.4、授权登录的实现思路
使用OAuth2 Login 比如登录淘宝可以使用微信登陆和支付宝登录

3、GiuHub社交登录案例
3.1、创建应用
注册客户应用:
登录GitHub,在开发者设置中找到OAuth Apps,创建一个application,为客户应用创建访问GitHub的凭据:

填写应用信息:默认的重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId}。registrationId是ClientRegistration的唯一标识符。

获取应用程序id,生成应用程序密钥:

3.2、创建测试项目
创建一个springboot项目oauth2-login-demo,创建时引入如下依赖

示例代码参考:spring-security-samples/servlet/spring-boot/java/oauth2/login at 6.2.x · spring-projects/spring-security-samples (github.com)
3.3、配置OAuth客户端属性
application.yml:
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: 7807cc3bb1534abce9f2
            client-secret: 008dc141879134433f4db7f62b693c4a5361771b
#            redirectUri: http://localhost:8200/login/oauth2/code/github
 
3.4、创建Controller
package com.atguigu.oauthdemo.controller;
@Controller
public class IndexController {
    @GetMapping("/")
    public String index(
            Model model,
            @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
            @AuthenticationPrincipal OAuth2User oauth2User) {
        model.addAttribute("userName", oauth2User.getName());
        model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
        model.addAttribute("userAttributes", oauth2User.getAttributes());
        return "index";
    }
}
 
3.5、创建html页面
resources/templates/index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <title>Spring Security - OAuth 2.0 Login</title>
    <meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
    <div style="float:left">
        <span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
    </div>
    <div style="float:none"> </div>
    <div style="float:right">
        <form action="#" th:action="@{/logout}" method="post">
            <input type="submit" value="Logout" />
        </form>
    </div>
</div>
<h1>OAuth 2.0 Login with Spring Security</h1>
<div>
    You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>
    via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>
</div>
<div> </div>
<div>
    <span style="font-weight:bold">User Attributes:</span>
    <ul>
        <li th:each="userAttribute : ${userAttributes}">
            <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
        </li>
    </ul>
</div>
</body>
</html>
 
3.6、启动应用程序
- 启动程序并访问localhost:8080。浏览器将被重定向到默认的自动生成的登录页面,该页面显示了一个用于GitHub登录的链接。
 - 点击GitHub链接,浏览器将被重定向到GitHub进行身份验证。
 - 使用GitHub账户凭据进行身份验证后,用户会看到授权页面,询问用户是否允许或拒绝客户应用访问GitHub上的用户数据。点击允许以授权OAuth客户端访问用户的基本个人资料信息。
 - 此时,OAuth客户端访问GitHub的获取用户信息的接口获取基本个人资料信息,并建立一个已认证的会话。
 
4、案例分析
4.1、登录流程
- A 网站让用户跳转到 GitHub,并携带参数ClientID 以及 Redirection URI。
 - GitHub 要求用户登录,然后询问用户"A 网站要求获取用户信息的权限,你是否同意?"
 - 用户同意,GitHub 就会重定向回 A 网站,同时发回一个授权码。
 - A 网站使用授权码,向 GitHub 请求令牌。
 - GitHub 返回令牌.
 - A 网站使用令牌,向 GitHub 请求用户数据。
 - GitHub返回用户数据
 - A 网站使用 GitHub用户数据登录
 

4.2、CommonOAuth2Provider
CommonOAuth2Provider是一个预定义的通用OAuth2Provider,为一些知名资源服务API提供商(如Google、GitHub、Facebook)预定义了一组默认的属性。
例如,授权URI、令牌URI和用户信息URI通常不经常变化。因此,提供默认值以减少所需的配置。
因此,当我们配置GitHub客户端时,只需要提供client-id和client-secret属性。
GITHUB {
    public ClientRegistration.Builder getBuilder(String registrationId) {
        ClientRegistration.Builder builder = this.getBuilder(
        registrationId, 
        ClientAuthenticationMethod.CLIENT_SECRET_BASIC, 
        
        //授权回调地址(GitHub向客户应用发送回调请求,并携带授权码)   
		"{baseUrl}/{action}/oauth2/code/{registrationId}");
        builder.scope(new String[]{"read:user"});
        //授权页面
        builder.authorizationUri("https://github.com/login/oauth/authorize");
        //客户应用使用授权码,向 GitHub 请求令牌
        builder.tokenUri("https://github.com/login/oauth/access_token");
        //客户应用使用令牌向GitHub请求用户数据
        builder.userInfoUri("https://api.github.com/user");
        //username属性显示GitHub中获取的哪个属性的信息
        builder.userNameAttributeName("id");
        //登录页面超链接的文本
        builder.clientName("GitHub");
        return builder;
    }
},
 
官方给的是整合github登录的列子 使用性不强,但是其他平台也是使用outh2 api作为俩调登录
 
 比如我这里登录获取token 返回令牌就是隐藏式的
 










