springsecurity整合oauth2

认证:用户输入账号密码,进行认证;

授权:登陆后,拿到当前用户的角色权限信息,给他进行分配权限,使他可以访问某些接口;

单体应用模式:就是单台机器;

用户-》请求:带着用户名密码-》认证-》访问数据库-》授权-》访问端点(EndPoint),可以认为是http接口,或者数据;

微服务模式:就是多台机器;

用户拿着用户名密码请求到-》授权中心Oauth2->访问数据库-》返回授权码或者令牌token->认证/校验->授权->访问端点(EndPoint);

http基础认证

通过http请求头,携带用户名密码进行登陆认证;

就是在f12控制台,在请求头中,显示Authorization: Basic base64加密的用户名密码;

在springboot2.4版本之前,SpringSecurity配置类需要继承WebSecurityConfigurerAdapter

在2.4之后就不需要在继承了,可以看到他上面已经报黑线了

在2.4之后使用下面的方式,安全筛选器链

我们来看下http基础认证的代码

<?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>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/>
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.11.graal</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!--spring security核心包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>


    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.example.demo.DemoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Configuration
public class SpringSecurityConfig  {

   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //对那些请求进行拦截
       http.authorizeRequests((x)->{
           //所有请求都拦截
           x.anyRequest().authenticated();
        })      //http基础认证 不认证无法访问
               .httpBasic(Customizer.withDefaults());
       //创建对象
       return http.build();
   }
}
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("/test")
    public String test(){
        return "aa";
    }
}

访问

http://localhost:8080/test

弹出这个框就表示http基础认证

用户名user

在源码这里,默认的用户名

密码在这里

在f12控制台可以看到,这就是http基础认证

Authorization: Basic dXNlcjpiNjgzNWI0YS01YTgyLTQ4NmMtOTIxNy0yODE0MmVkNDExNDA=

我们测试一下,不带请求头访问看看啥效果

可以看到401,没有权限

我们带上请求头看看啥效果

可以看到已经返回数据了

或者可以在这里输入账号密码

应用场景:给别的服务,在后端调用的时候使用,不需要前台登陆

接下来我们在看下表单模式

这个样子就是表单模式

.formLogin()就是使用表单模式登陆

接下来我们引入mysql来实现自定义认证

创建用户表

CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `enabled` int(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加一条测试数据

INSERT INTO `sys_user` (`id`, `account`, `password`, `name`, `enabled`) VALUES ('1', 'zhangsan', '123456', '张三', '1');
<?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>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/>
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.11.graal</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!--spring security核心包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 添加MyBatisPlus的依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- MySQL数据 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- druid  连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.14</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>


    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.example.demo.DemoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
server:
  port: 8080
spring:
  application:
    name: yewu
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://192.168.23.131:13306/db1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
package com.example.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.entity.SysUser;
import com.example.demo.mapper.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * UserDetailsService 用户查询接口
 *
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Service
public class SysUserService implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;


    /**
     * 根据账号查询用户信息
     * @param
     * @return
     * @throws Exception
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<SysUser>queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("account",username);
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
        if(sysUser==null){
            log.info("用户不存在");
            throw new UsernameNotFoundException("用户不存在");
        }
        //因为我们在实体类已经实现了UserDetails接口 所以可以直接返回
        return sysUser;
    }
}
package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Arrays;
import java.util.Collection;

/**
 * UserDetails 用户详细信息接口
 *
 * @param
 * @return
 * @throws Exception
 */
@Data
@TableName("sys_user")
public class SysUser implements UserDetails {

    @TableId(type = IdType.AUTO)
    private Integer id;

    //账号
    private String account;

    //密码
    private String password;

    //姓名
    private String name;

    //是否启用 0:否  1:是
    private Integer enabled;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.account;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //是否启用 0:否  1:是
    @Override
    public boolean isEnabled() {
        if(this.enabled==1){
            return true;
        }
        return false;
    }
}
package com.example.demo.config;

import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 *
 * 实现身份验证提供程序
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private PasswordEncoder passwordEncoder;



    /**
     *
     * 登陆认证
     * @param
     * @return
     * @throws Exception
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //从authentication获取用户名和凭证(密码)信息
        String username=authentication.getName();
        String password=authentication.getCredentials().toString();
        log.info("密码=========================={}",password);
        //查询用户是否存在
        UserDetails userDetails = sysUserService.loadUserByUsername(username);
        //比较和数据库的密码是否一样
        if(passwordEncoder.matches(password,userDetails.getPassword())){
            //返回用户名密码认证令牌
            //因为UsernamePasswordAuthenticationToken的上级父类的父类是Authentication 所以可以直接返回
            return new UsernamePasswordAuthenticationToken(username,password,userDetails.getAuthorities());
        }else {
            throw new BadCredentialsException("用户名或者密码错误了");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //保证认证和返回的对象都是UsernamePasswordAuthenticationToken
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}
package com.example.demo.config;

import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Configuration
public class SpringSecurityConfig  {

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //对那些请求进行拦截
       http.formLogin()
               .and()
               .authorizeRequests()
               .anyRequest()
               .authenticated();
       //创建对象
       return http.build();
   }
}

生成密码 放入数据库中

package com.example.demo.config;

import org.apache.tomcat.util.security.MD5Encoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

public class Test {


    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123456");
        System.out.println(encode);
    }
}

http://localhost:8080/test

再次登陆 输入zhangsan 123456 登陆成功

输入错误的账号密码 登陆失败

当我们使用下面的代码的时候,每次都需要一次登陆,适合http基础认证,保证了接口的安全

//无状态的 不会创建session会话 每次都进行新的认证

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

当使用表单登陆的时候,就需要session会话了,不用每次都登陆

接下来我们创建一个自定义的登陆界面

在static目录下创建login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form method="post" action="/dengLu">
    账号:<input type="text" name="account" value="">
    密码:<input type="text" name="password" value="">
    <input type="submit" value="登陆">
</form>

</body>
</html>

package com.example.demo.config;

import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Configuration
public class SpringSecurityConfig  {

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //启用会话存储 在需要时创建session会话
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

       //对那些请求进行拦截
       http.authorizeRequests()
               //任何请求都拦截
               .anyRequest().authenticated()
               .and()
               //启用表单认证模式
               .formLogin()
               //登陆界面
               .loginPage("/login.html")
               //登陆点击提交后的地址
               .loginProcessingUrl("/dengLu")
               //放行 loginPage和loginProcessingUrl 不用登陆
               .permitAll()
               //设置username的别名为 用于在登陆的html表单的名字
               .usernameParameter("account")
               //设置password的别名为 用于在登陆的html表单的名字
               .passwordParameter("password")
               .and()
               //设置注销功能
               .logout()
               //注销url地址
               .logoutUrl("/logout")
               //注销后跳转地址
               .logoutSuccessUrl("/login.html")
               //session直接过期
               .invalidateHttpSession(true)
               //清除认证信息
               .clearAuthentication(true)
               .and()
               //禁用csrf安全防护
               .csrf().disable();
       //创建对象
       return http.build();
   }
}

这一部分是登陆信息

这一部分是注销信息

每一部分都用.and来开启新的设置

我们可以在源码看到,默认的用户名和密码字段,就叫这个

然后我们再次登陆

http://localhost:8080/test

自动跳转到了,我们自定义的登陆界面

输入退出

http://localhost:8080/logout

退出后又跳转到了登陆界面

然后我们在访问

http://localhost:8080/test

发现退出后,就需要再次登陆了

当我们使用微服务的时候,就不在使用自带的html了,而是使用vue前后端分离的场景

接下来我们来看下,怎么返回json数据

我们先把登陆地址和注销后跳转地址注释掉,因为已经用不到了

package com.example.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 我的身份验证入口点
 * 没有登陆认证 异常处理器
 * @param
 * @return
 * @throws Exception
 */
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //返回json格式
        response.setContentType("application/json;charset=utf-8");
        //没有登陆 直接访问其他接口 就报401
        response.setStatus(401);
        Map<String,Object> map=new HashMap<>();
        map.put("code",401);
        map.put("msg","访问此资源需要完全身份验证");
        ObjectMapper objectMapper=new ObjectMapper();
        String s=objectMapper.writeValueAsString(map);
        //把json数据 写入 返回给前端
        PrintWriter writer=response.getWriter();
        writer.write(s);
        writer.flush();
        writer.close();
    }
}

package com.example.demo.config;

import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Configuration
public class SpringSecurityConfig  {

    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //启用会话存储 在需要时创建session会话
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

       //把没有登陆 直接访问的 异常信息添加进来
       http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
               //任何请求都拦截
               .and().
               authorizeRequests()
               .anyRequest().authenticated()
               .and()
               //启用表单认证模式
               .formLogin()
               //登陆界面 废弃
              // .loginPage("/login.html")
               //登陆点击提交后的地址
               .loginProcessingUrl("/dengLu")
               //放行 loginPage和loginProcessingUrl 不用登陆
               .permitAll()
               //设置username的别名为 用于在登陆的html表单的名字
               .usernameParameter("account")
               //设置password的别名为 用于在登陆的html表单的名字
               .passwordParameter("password")
               .and()
               //设置注销功能
               .logout()
               //注销url地址
               .logoutUrl("/logout")
               //注销后跳转地址
              // .logoutSuccessUrl("/login.html")
               //session直接过期
               .invalidateHttpSession(true)
               //清除认证信息
               .clearAuthentication(true)
               .and()
               //禁用csrf安全防护
               .csrf().disable();
       //创建对象
       return http.build();
   }
}

http://localhost:8080/test

我们再次访问,没有登陆,会报下面的json格式的提示信息

接下来我们在看登陆成功和失败的提示信息

package com.example.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 登陆成功后返回的提示信息
 * 身份验证成功处理程序
 * @param
 * @return
 * @throws Exception
 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //返回json格式
        response.setContentType("application/json;charset=utf-8");
        //没有登陆 直接访问其他接口 就报401
        response.setStatus(200);
        Map<String,Object> map=new HashMap<>();
        map.put("code",200);
        map.put("msg","登陆成功");
        ObjectMapper objectMapper=new ObjectMapper();
        String s=objectMapper.writeValueAsString(map);
        //把json数据 写入 返回给前端
        PrintWriter writer=response.getWriter();
        writer.write(s);
        writer.flush();
        writer.close();
    }
}
package com.example.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 登陆失败返回的提示信息
 * 身份验证失败处理程序
 * @param
 * @return
 * @throws Exception
 */
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //返回json格式
        response.setContentType("application/json;charset=utf-8");
        //没有登陆 直接访问其他接口 就报401
        response.setStatus(403);
        Map<String,Object> map=new HashMap<>();
        map.put("code",403);
        map.put("msg","登陆失败");
        ObjectMapper objectMapper=new ObjectMapper();
        String s=objectMapper.writeValueAsString(map);
        //把json数据 写入 返回给前端
        PrintWriter writer=response.getWriter();
        writer.write(s);
        writer.flush();
        writer.close();
    }
}
package com.example.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 注销成功处理程序
 * 注销成功提示信息
 * @param
 * @return
 * @throws Exception
 */
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //返回json格式
        response.setContentType("application/json;charset=utf-8");
        //没有登陆 直接访问其他接口 就报401
        response.setStatus(200);
        Map<String,Object> map=new HashMap<>();
        map.put("code",200);
        map.put("msg","注销成功");
        ObjectMapper objectMapper=new ObjectMapper();
        String s=objectMapper.writeValueAsString(map);
        //把json数据 写入 返回给前端
        PrintWriter writer=response.getWriter();
        writer.write(s);
        writer.flush();
        writer.close();
    }
}
package com.example.demo.config;

import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Configuration
public class SpringSecurityConfig  {

    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //启用会话存储 在需要时创建session会话
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

       //把没有登陆 直接访问的 异常信息添加进来
       http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
               //任何请求都拦截
               .and().
               authorizeRequests()
               .anyRequest().authenticated()
               .and()
               //启用表单认证模式
               .formLogin()
               //登陆成功返回的提示信息
               .successHandler(myAuthenticationSuccessHandler)
               //登陆失败的返回提示信息
               .failureHandler(myAuthenticationFailureHandler)
               //登陆界面 废弃
              // .loginPage("/login.html")
               //登陆点击提交后的地址
               .loginProcessingUrl("/dengLu")
               //放行 loginPage和loginProcessingUrl 不用登陆
               .permitAll()
               //设置username的别名为 用于在登陆的html表单的名字
               .usernameParameter("account")
               //设置password的别名为 用于在登陆的html表单的名字
               .passwordParameter("password")
               .and()
               //设置注销功能
               .logout()
               //注销成功的提示信息
               .logoutSuccessHandler(myLogoutSuccessHandler)
               //注销url地址
               .logoutUrl("/logout")
               //注销后跳转地址
              // .logoutSuccessUrl("/login.html")
               //session直接过期
               .invalidateHttpSession(true)
               //清除认证信息
               .clearAuthentication(true)
               .and()
               //禁用csrf安全防护
               .csrf().disable();
       //创建对象
       return http.build();
   }
}

http://localhost:8080/dengLu

可以看到 登陆成功了

在看下登陆失败的

在看下注销成功的

主要关注这3个地方,登陆成功失败,必须在formLogin下面,注销成功必须在logout下面

我们在看下 登陆后获取用户信息

package com.example.demo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class TestController {

    @GetMapping("/test")
    public String test(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("用户账号:{}",authentication.getName());
        log.info("主体信息:{}",authentication.getPrincipal());
        log.info("权限信息:{}",authentication.getAuthorities());
        return "success";
    }
}

权限是我们后台写死的

接下来我们看下自定义过滤器

package com.example.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 *
 * 在之后处理的过滤器
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
public class AfterFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("在登陆之后的过滤器");
        //继续往下走
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
package com.example.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 *
 * 在之前处理的过滤器
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
public class BeforeFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("在登陆之前的过滤器");
        //继续往下走
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
package com.example.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 *
 * 自定义处理的过滤器
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
public class CustomFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("自定义处理的过滤器");
        //继续往下走
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
package com.example.demo.config;

import com.example.demo.filter.AfterFilter;
import com.example.demo.filter.BeforeFilter;
import com.example.demo.filter.CustomFilter;
import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Configuration
public class SpringSecurityConfig  {

    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //启用会话存储 在需要时创建session会话
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

       //把没有登陆 直接访问的 异常信息添加进来
       http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
               //任何请求都拦截
               .and().
               authorizeRequests()
               .anyRequest().authenticated()
               .and()
               //启用表单认证模式
               .formLogin()
               //登陆成功返回的提示信息
               .successHandler(myAuthenticationSuccessHandler)
               //登陆失败的返回提示信息
               .failureHandler(myAuthenticationFailureHandler)
               //登陆界面 废弃
              // .loginPage("/login.html")
               //登陆点击提交后的地址
               .loginProcessingUrl("/dengLu")
               //放行 loginPage和loginProcessingUrl 不用登陆
               .permitAll()
               //设置username的别名为 用于在登陆的html表单的名字
               .usernameParameter("account")
               //设置password的别名为 用于在登陆的html表单的名字
               .passwordParameter("password")
               .and()
               //设置注销功能
               .logout()
               //注销成功的提示信息
               .logoutSuccessHandler(myLogoutSuccessHandler)
               //注销url地址
               .logoutUrl("/logout")
               //注销后跳转地址
              // .logoutSuccessUrl("/login.html")
               //session直接过期
               .invalidateHttpSession(true)
               //清除认证信息
               .clearAuthentication(true)
               .and()
               //禁用csrf安全防护
               .csrf().disable();

       //在登陆之后的过滤器
       http.addFilterAfter(new AfterFilter(), UsernamePasswordAuthenticationFilter.class);
       //在登陆之前的过滤器
       http.addFilterBefore(new BeforeFilter(),UsernamePasswordAuthenticationFilter.class);
       //自定义过滤器
       http.addFilterAt(new CustomFilter(),UsernamePasswordAuthenticationFilter.class);
       //创建对象
       return http.build();
   }
}

主要关注这里

我们来登陆一下

在登陆之前只打印了这2个过滤器

在获取下用户信息

这次3个过滤器都打印出来了

我们可以在这里判断那些角色可以访问

.authenticated()注释掉

添加.access("hasAnyAuthority('ROLE_USER1')")

对应用户的这里

如果没有权限就会显示403

我们也可以判断这个接口有没有角色访问

.access("hasAnyRole('USER')")

在这里不用加ROLE_,我们点击这个hasAnyRole进入源码

在源码里面会自动拼接上

接下来我们在看下匹配器

在这里可以使他禁止访问 403

.mvcMatchers("/test").access("denyAll()")

也可以模糊匹配,使他能访问

.mvcMatchers("/te*").access("permitAll()")

这里要注意匹配器要放在.anyRequest()前面,否则报错

Can't configure mvcMatchers after anyRequest

无法在anyRequest之后配置mvcMatchers

接下来我们在看下在方法上面授权

在配置类加入这个

//启用全局方法安全 启用预发布 可以使用注解在方法上了加权限 使他生效

@EnableGlobalMethodSecurity(prePostEnabled = true)

package com.example.demo.config;

import com.example.demo.filter.AfterFilter;
import com.example.demo.filter.BeforeFilter;
import com.example.demo.filter.CustomFilter;
import com.example.demo.service.impl.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Configuration
//启用全局方法安全 启用预发布 可以使用注解在方法上了加权限 使他生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig  {

    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


   /**
    *
    * 安全过滤器链
    * @param
    * @return
    * @throws Exception
    */
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       //启用会话存储 在需要时创建session会话
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
       //把没有登陆 直接访问的 异常信息添加进来
       http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
               //任何请求都拦截
               .and().
               authorizeRequests()
               .anyRequest()
               .authenticated()
               .and()
               //启用表单认证模式
               .formLogin()
               //登陆成功返回的提示信息
               .successHandler(myAuthenticationSuccessHandler)
               //登陆失败的返回提示信息
               .failureHandler(myAuthenticationFailureHandler)
               //登陆点击提交后的地址
               .loginProcessingUrl("/dengLu")
               //放行 loginPage和loginProcessingUrl 不用登陆
               .permitAll()
               //设置username的别名为 用于在登陆的html表单的名字
               .usernameParameter("account")
               //设置password的别名为 用于在登陆的html表单的名字
               .passwordParameter("password")
               .and()
               //设置注销功能
               .logout()
               //注销成功的提示信息
               .logoutSuccessHandler(myLogoutSuccessHandler)
               //注销url地址
               .logoutUrl("/logout")
               //session直接过期
               .invalidateHttpSession(true)
               //清除认证信息
               .clearAuthentication(true)
               .and()
               //禁用csrf安全防护
               .csrf().disable();

       //在登陆之后的过滤器
       http.addFilterAfter(new AfterFilter(), UsernamePasswordAuthenticationFilter.class);
       //在登陆之前的过滤器
       http.addFilterBefore(new BeforeFilter(),UsernamePasswordAuthenticationFilter.class);
       //自定义过滤器
       http.addFilterAt(new CustomFilter(),UsernamePasswordAuthenticationFilter.class);



       //创建对象
       return http.build();
   }
}
package com.example.demo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class TestController {

    @GetMapping("/test")
    public String test(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("用户账号:{}",authentication.getName());
        log.info("主体信息:{}",authentication.getPrincipal());
        log.info("权限信息:{}",authentication.getAuthorities());
        return "success";
    }


    @PreAuthorize("hasAnyRole('USER1')")
    @GetMapping("/aa")
    public String aa(){
        return "aaaaaaaaaaaaaaaaaaaaaaa";
    }
}

在这里加入不同的角色权限判断

如果没有权限就会403

接下来我们来看下Oauth2协议

他有4种模式,授权码模式,客户端模式,密码模式,隐式授权模式,

我们常用的就是授权码模式

我们来看下什么是授权码模式

用户请求首页-》需要先跳转到第三方登陆-》获取授权服务器的token-》然后拿着token去资源服务器进行获取信息

我们来看下什么是客户端模式

就是需要先在第三方平台上注册好客户端的信息-》拿着客户端id和秘钥进行登陆-》授权服务器处理信息

其他2种模式用不到,不做介绍

接下来我们先在C:\Windows\System32\drivers\etc

配置好hosts

这样就能避免cookie 相同域名(localhost)覆盖的bug

127.0.0.1 auth-server
127.0.0.1 res-server
127.0.0.1 client

接下来我们先创建授权服务器

<?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.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>auth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>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>
        <!--授权服务-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
            <version>0.3.1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
package com.example.authserver.security;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
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.OAuth2AuthorizationServerConfiguration;
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.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
//开启web安全 应用在web环境下
// 1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略
// 2: 加载了AuthenticationConfiguration, 配置了认证信息
@EnableWebSecurity
public class SecurityConfig {


    /**
     *
     * 授权服务安全过滤器链
     * 第一个进来
     * @param
     * @return
     * @throws Exception
     */
    @Order(1)
    @Bean
    public SecurityFilterChain authorizedServiceSecurityFilterChain(HttpSecurity http) throws Exception {
        //授权服务配置 应用默认安全性 简化配置,在源码给你都配置好了
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.   //异常处理
                exceptionHandling().
                //设置默认的登陆界面
                authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"));
        return http.build();
    }

    /**
     * 默认安全过滤器链
     * 用于身份认证
     * 第二个进入
     * @param
     * @return
     * @throws Exception
     */
    @Order(2)
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        //必须认证才能登陆
        http.authorizeHttpRequests()
                .anyRequest()
                .authenticated()
                .and()
                //采用表单认证方式登陆
                .formLogin(Customizer.withDefaults());
        return http.build();
    }


    /**
     *
     * 配置用户信息
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public UserDetailsService userDetailsService(){
        UserDetails userDetails= User.withDefaultPasswordEncoder()
                //用户名 账号
                .username("zhangsan")
                //密码
                .password("123456")
                .roles("USER")
                .build();
        //内存用户详细信息管理器  不去使用数据库
        return new InMemoryUserDetailsManager(userDetails);
    }


    /**
     *
     *  用于第三方认证
     *  主要管理第三方的客户端
     *  已注册客户端存储库
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(){
        RegisteredClient registeredClient=RegisteredClient.withId(UUID.randomUUID().toString())
                //客户端id
                .clientId("showDoc")
                //{noop}不加密,客户端密码
                .clientSecret("{noop}123456")
                //客户端认证方法  客户端安全基本 客户端通过客户端密码认证方式接入
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                //客户端允许使用的授权模式,授权类型,授权码模式,刷新token,客户端模式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                //客户端允许跳转的uri注册地址,回调地址 登陆成功之后跳转的地址
                .redirectUri("http://auth-server:8080/authorized")
                //.redirectUri("http://client:8082/login/oauth2/code/demo")
                //客户端允许使用的范围授权 如果对方写的不对 那么就没有权限进行登陆
                .scope("read")
                .scope("write")
                //是否开启用户手动确认,false为自动确认 就是在授权界面 是否选择 复选框
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();
        //内存注册客户端存储  不查询数据库
        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    /**
     *
     * 通过非对称加密生成access_token(jwt)的签名部分
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public JWKSource<SecurityContext>jwkSource(){
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * ⽣成秘钥对,为jwkSource提供服务,私钥服务器⾃身持有,公钥对外开放。
     *
     * @return
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     *
     * 提供程序设置 默认配置
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public ProviderSettings providerSettings(){
        return ProviderSettings.builder().build();
    }

}

使用这个链接进行访问授权地址,get请求

http://auth-server:8080/oauth2/authorize?response_type=code&client_id=showDoc&scope=read&redirect_uri=http://auth-server:8080/authorized

自动跳转到登陆界面

输入用户zhangsan 密码123456

选择read 添加提交进行授权

就是对应的这里,如果为true显示授权界面,如果为false 不显示 默认自动授权

这个时候我们看到404,是因为我们还没有写http://auth-server:8080/authorized接口

就是对应的这里

创建接口

package com.example.authserver.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {



    /**
     *
     * 获取授权码
     * @param
     * @return
     * @throws Exception
     */
    @GetMapping("/authorized")
    public String authorized(String code){
        return code;
    }
}

我们在来一次 获取授权码

http://auth-server:8080/oauth2/authorize?response_type=code&client_id=showDoc&scope=read&redirect_uri=http://auth-server:8080/authorized

可以看到拿到了授权码

我们在把这里改成false,可以看到就自动跳到获取授权码的界面了

接下来我们创建资源服务器

比如说我们想看到外卖的信息,这些外卖的信息就是放在资源服务器的

<?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.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>res-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>res-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--资源服务-->
        <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-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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.resserver.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.web.SecurityFilterChain;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Configuration
public class SecurityConfig {


    /**
     *
     * 安全过滤器链
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //拦截所有请求
        http.authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    //oauth2资源服务器 使用jwt 带着jwt的token访问资源服务器
                    .oauth2ResourceServer().jwt();
            return http.build();
    }

}
package com.example.resserver.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResController {



    @GetMapping("/hello")
    public String hello(){
        return "你好啊。。。。。。。。。。。。。。。";
    }
}

配置文件

server:
  port: 8081
logging:
  level:
    root: debug
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          #获取公钥的路径 授权服务器的暴露的路径 不需要自己开发
          jwk-set-uri: http://auth-server:8080/oauth2/jwks

post请求,接下来拿着授权码获取令牌

http://auth-server:8080/oauth2/token?redirect_uri=http://auth-server:8080/authorized&grant_type=authorization_code&code=3-JjPZUTZfGsZ6C49R9eqbU8XHIndMZwn55SpCuhnSJ6RZSF4Je_EgiV5bismaeg49IgFLCPURFqAI9DhZ6c61O0pLrzx2R5cWy4YcXX-Gmb_vqJZMuDqcR93rN5TKZz

注意这里对应的我们的客户端的id,秘钥

对应的这里

授权码只能使用一次,如果长时间不用,一会就过期了

access_token就是我们要访问资源服务器获取接口的时候要传的值

refresh_token 就是令牌过期了,刷新的token

scope 授权的作用域

token_type token类型Bearer,传参数时候需要用到

expires_in 过期时间,300/60=5分钟

接下来我们访问资源服务器,获取接口信息

http://res-server:8081/hello

access_token放入这里

我们在浏览器访问

http://auth-server:8080/oauth2/jwks

如果在资源服务器不去配置jwk-set-uri,那么就会无法解密

jwk在oidc中的主要作用是为jwt(id_token)提供加密秘钥,用于加密、解密,或签名,验签,是json格式的数据;

jwks是指多个jwk组合在一起的一种格式;

授权服务器生成签名:Sign=RS256(Base64Encode(x)+Base64Encode(y),'私钥');

资源服务器拿着http://auth-server:8080/oauth2/jwks 去获取公钥;然后重新生成签名,Sign=RS256(Base64Encode(x)+Base64Encode(y),'公钥');

和私钥的签名进行对比,如果不正确,那么说明jwt中途被篡改,直接拒绝访问,这样就能保证数据的安全

RSA算法是一种非对称加密,我们在授权服务器这里使用的

接下来我们在资源服务器进行获取用户信息的接口

package com.example.resserver.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

@RestController
public class ResController {



    @GetMapping("/hello")
    public String hello(){
        return "你好啊。。。。。。。。。。。。。。。";
    }


    /**
     * 资源服务获取用户信息
     * @param
     * @return
     * @throws Exception
     */
    @GetMapping("/getUser")
    public Map<String,String> getUser(Principal principal){
        Map<String,String>map=new HashMap<>();
        map.put("name",principal.getName());
        return map;
    }
}

http://res-server:8081/getUser

可以看到在资源服务器也能拿到用户的账号

接下来我们创建客户端

客户端的意思就是比如我是库存系统,我自己没有登陆,但是我想要登陆,那么就需要先在授权服务器进行注册客户端,然后才能拿着授权服务器的登陆,接入到我自己的业务系统,这样我就可以进行单点登陆了

<?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.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>client</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </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>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
package com.example.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ClientApplication {

    /**
     *
     * 用于远程通信 远程接口调用
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }

}
package com.example.client.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@Slf4j
@RestController
public class ClientControler {

    @Autowired
    private RestTemplate restTemplate;

    /**
     *
     * 获取资源服务器的接口
     *
     * RegisteredOAuth2AuthorizedClient 注册在授权服务器的客户端
     * @param
     * @return
     * @throws Exception
     */
    @GetMapping("/getResHello")
    public String getResUser(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client){
        HttpHeaders headers=new HttpHeaders();
        //从授权客户端拿到token
        String token=client.getAccessToken().getTokenValue();
        log.info("令牌为:{}",token);
        //把token 放入请求头中 ,token类型为Bearer
        //在源码里面this.set("Authorization", "Bearer " + token) 自动拼接Bearer空格
        headers.setBearerAuth(token);
        HttpEntity<String>request=new HttpEntity<>("",headers);
        //拿着令牌去获取资源服务器的接口
        ResponseEntity<String>responseEntity=restTemplate
                .exchange("http://res-server:8081/hello",
                        HttpMethod.GET,request,String.class);
        String body=responseEntity.getBody();
        return body;
    }
}
package com.example.client.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.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {


    /**
     *
     * 安全过滤器链
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //拦截所有请求
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                //使用oauth2登陆,自动跳转到授权服务的登陆界面
                .oauth2Login();
        return http.build();
    }
}
server:
  port: 8082
spring:
  security:
    oauth2:
      client:
        #注册客户端
        registration:
          #注册名称 随便起
          test:
            #客户端id 在授权服务器注册好的客户端
            client-id: showDoc
            #客户端秘钥
            client-secret: 123456
            #供应商 要对应下面的供应商一致的名字
            provider: test-provider
            #授权类型 为授权码模式
            authorization-grant-type: authorization_code
            #回调地址 接收授权码的uri规则: http://client:8082/login/oauth/code/{profile-name}
            #这个路径不用自己开发 默认的, test 可以换成你的注册名称 要和上面的registration下面的test一致
            #在访问的时候会自动把test忽略掉,他去找test注册名称 这一套配置
            redirect_uri: http://client:8082/login/oauth2/code/test
            #作用域 授权 复选框那个
            scope:
              - read
        #供应商
        provider:
          #供应商的名称 要和注册哪里的供应商名称一致
          test-provider:
            #授权接口 不需要自己开发
            authorization-uri: http://auth-server:8080/oauth2/authorize
            #获取token令牌 不需要自己开发
            token-uri: http://auth-server:8080/oauth2/token
            #获取用户信息接口 需要自己开发 我们在资源服务器 已经开发好了
            user-info-uri: http://res-server:8081/getUser
            #用户名属性 资源服务器获取用户信息里面 map里面的name的字段 来给到对方
            user-name-attribute: name
  codec:
    #日志请求详细信息
    log-request-details: true

logging:
  level:
    root: debug



然后我们在授权服务器的安全配置类里面把这一行回调地址注册上

.redirectUri("http://client:8082/login/oauth2/code/test")

把3分服务都启动

访问http://client:8082/getResHello

可以看到自动跳转到了授权服务器的登陆,这就是单点登陆

当输入客户端地址后回车的时候,自动跳转到授权服务器的路径

带着客户端id,授权码code,scope,状态,回调地址,去请求授权

接下来我们输入账号zhangsan,密码123456

可以看到,拿到了资源服务器的信息

我们在看下控制台日志

登陆之后,获取令牌,拿着令牌获取用户信息

然后再重定向回来客户端的路径

然后拿着令牌远程访问资源服务器的数据

接下来我们在使用数据库

先创建用户表,和用户权限表

CREATE TABLE `users` (
  `username` varchar(50) NOT NULL,
  `PASSWORD` varchar(500) NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
CREATE TABLE `authorities` (
  `username` varchar(50) NOT NULL,
  `authority` varchar(50) NOT NULL,
  UNIQUE KEY `ix_auth_username` (`username`,`authority`),
  CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户权限表';

对应这个包的pgsql,要改成mysql的语法

然后再创建用户手动确认授权同意书表

CREATE TABLE `oauth2_authorization_consent` (
  `registered_client_id` varchar(100) NOT NULL,
  `principal_name` varchar(200) NOT NULL,
  `authorities` varchar(1000) NOT NULL,
  PRIMARY KEY (`registered_client_id`,`principal_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户手动确认授权同意书';

创建客户端授权纪录表(包含授权码,JWT等信息)

CREATE TABLE `oauth2_authorization` (
  `id` varchar(100) NOT NULL,
  `registered_client_id` varchar(100) NOT NULL,
  `principal_name` varchar(200) NOT NULL,
  `authorization_grant_type` varchar(100) NOT NULL,
  `attributes` blob,
  `state` varchar(500) DEFAULT NULL,
  `authorization_code_value` blob,
  `authorization_code_issued_at` datetime DEFAULT NULL,
  `authorization_code_expires_at` datetime DEFAULT NULL,
  `authorization_code_metadata` blob,
  `access_token_value` blob,
  `access_token_issued_at` datetime DEFAULT NULL,
  `access_token_expires_at` datetime DEFAULT NULL,
  `access_token_metadata` blob,
  `access_token_type` varchar(100) DEFAULT NULL,
  `access_token_scopes` varchar(1000) DEFAULT NULL,
  `oidc_id_token_value` blob,
  `oidc_id_token_issued_at` datetime DEFAULT NULL,
  `oidc_id_token_expires_at` datetime DEFAULT NULL,
  `oidc_id_token_metadata` blob,
  `refresh_token_value` blob,
  `refresh_token_issued_at` datetime DEFAULT NULL,
  `refresh_token_expires_at` datetime DEFAULT NULL,
  `refresh_token_metadata` blob,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='客户端授权纪录表(包含授权码,JWT等信息)';

创建客户端注册信息表

CREATE TABLE `oauth2_registered_client` (
  `id` varchar(100) NOT NULL,
  `client_id` varchar(100) NOT NULL,
  `client_id_issued_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `client_secret` varchar(200) DEFAULT NULL,
  `client_secret_expires_at` datetime DEFAULT NULL,
  `client_name` varchar(200) NOT NULL,
  `client_authentication_methods` varchar(1000) NOT NULL,
  `authorization_grant_types` varchar(1000) NOT NULL,
  `redirect_uris` varchar(1000) DEFAULT NULL,
  `scopes` varchar(1000) NOT NULL,
  `client_settings` varchar(2000) NOT NULL,
  `token_settings` varchar(2000) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='客户端注册信息表';

在源码的这个位置

然后在授权服务器加上这些包


        <!-- MySQL数据 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
      
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

然后在授权服务器 加上这些配置

logging:
  level:
    root: debug

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
server:
  port: 8080
spring:
  application:
    name: auth-server
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    password: 123456
    url: jdbc:mysql://192.168.23.131:13306/db2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root

然后再授权服务器的安全配置类,修改用户和客户端查询数据库

package com.example.authserver.security;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
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.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
//开启web安全 应用在web环境下
// 1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略
// 2: 加载了AuthenticationConfiguration, 配置了认证信息
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private JdbcTemplate jdbcTemplate;




    /**
     *
     * 授权服务安全过滤器链
     * 第一个进来
     * @param
     * @return
     * @throws Exception
     */
    @Order(1)
    @Bean
    public SecurityFilterChain authorizedServiceSecurityFilterChain(HttpSecurity http) throws Exception {
        //授权服务配置 应用默认安全性 简化配置,在源码给你都配置好了
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.   //异常处理
                exceptionHandling().
                //设置默认的登陆界面
                authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"));
        return http.build();
    }

    /**
     * 默认安全过滤器链
     * 用于身份认证
     * 第二个进入
     * @param
     * @return
     * @throws Exception
     */
    @Order(2)
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        //必须认证才能登陆
        http.authorizeHttpRequests()
                .anyRequest()
                .authenticated()
                .and()
                //采用表单认证方式登陆
                .formLogin(Customizer.withDefaults());
        return http.build();
    }


    /**
     *
     * 配置用户信息 从数据库查询 实现用户的crud操作
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public UserDetailsService userDetailsService(){
        return new JdbcUserDetailsManager(dataSource);
    }





    /**
     *
     *  用于第三方认证
     *  主要管理第三方的客户端
     *  已注册客户端存储库
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(){
        //从数据库查询已经注册的客户端信息
        return new JdbcRegisteredClientRepository(jdbcTemplate);
//        RegisteredClient registeredClient=RegisteredClient.withId(UUID.randomUUID().toString())
//                //客户端id
//                .clientId("showDoc")
//                //{noop}不加密,客户端密码
//                .clientSecret("{noop}123456")
//                //客户端认证方法  客户端安全基本 客户端通过客户端密码认证方式接入
//                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//                //客户端允许使用的授权模式,授权类型,授权码模式,刷新token,客户端模式
//                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
//                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//                //客户端允许跳转的uri注册地址,回调地址 登陆成功之后跳转的地址
//                .redirectUri("http://auth-server:8080/authorized")
//                .redirectUri("http://client:8082/login/oauth2/code/test")
//                //客户端允许使用的范围授权 如果对方写的不对 那么就没有权限进行登陆
//                .scope("read")
//                .scope("write")
//                //是否开启用户手动确认,false为自动确认 就是在授权界面 是否选择 复选框
//                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
//                .build();
//        //内存注册客户端存储  不查询数据库
//        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    /**
     *
     * 通过非对称加密生成access_token(jwt)的签名部分
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public JWKSource<SecurityContext>jwkSource(){
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * ⽣成秘钥对,为jwkSource提供服务,私钥服务器⾃身持有,公钥对外开放。
     *
     * @return
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     *
     * 提供程序设置 默认配置
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public ProviderSettings providerSettings(){
        return ProviderSettings.builder().build();
    }

    /**
     *
     * 对应 oauth2_authorization表 授权服务
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public OAuth2AuthorizationService auth2AuthorizationService(){
        return new JdbcOAuth2AuthorizationService(jdbcTemplate,registeredClientRepository());
    }

    /**
     * 对应oauth2_authorization_consent表
     * 用户确认授权同意书
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public OAuth2AuthorizationConsentService auth2AuthorizationConsentService(){
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate,registeredClientRepository());
    }
}

主要新增这4部分

我们先授权服务器初始化 一个用户 一个客户端 在测试类

package com.example.authserver;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@SpringBootTest
class AuthServerApplicationTests {
        @Autowired
        private UserDetailsManager userDetailsManager;
        @Autowired
        private RegisteredClientRepository registeredClientRepository;




        /**
         *
         * 创建用户
         * @param
         * @return
         * @throws Exception
         */
        @Test
        public void createUser(){
            //添加到用户表,和用户权限表中
            UserDetails userDetails= User.builder().passwordEncoder(
                    x->"{bcrypt}"+new BCryptPasswordEncoder().encode(x))
                    .username("zhangsan")
                    .password("123456")
                    .roles("USER")
                    .build();
            userDetailsManager.createUser(userDetails);
        }

        /**
         *
         * 注册客户端
         * @param
         * @return
         * @throws Exception
         */
        @Test
        public void registerClient(){
            RegisteredClient registeredClient=RegisteredClient.withId(UUID.randomUUID().toString())
                    //客户端id
                    .clientId("showDoc")
                    //客户端密码 加密
                    .clientSecret("{bcrypt}"+new BCryptPasswordEncoder().encode("123456"))
                    //客户端认证方法  客户端安全基本 客户端通过客户端密码认证方式接入
                    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                    //客户端允许使用的授权模式,授权类型,授权码模式,刷新token,客户端模式
                    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                    .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                    //客户端允许跳转的uri注册地址,回调地址 登陆成功之后跳转的地址
                    .redirectUri("http://auth-server:8080/authorized")
                    .redirectUri("http://client:8082/login/oauth2/code/test")
                    //客户端允许使用的范围授权 如果对方写的不对 那么就没有权限进行登陆
                    .scope("read")
                    .scope("write")
                    //是否开启用户手动确认,false为自动确认 就是在授权界面 是否选择 复选框
                    .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                    .build();
            //保存到oauth2_registered_client表里
            registeredClientRepository.save(registeredClient);
        }

    }



数据库就有数据了

然后用zhangsan,密码123456来登陆,进行获取资源信息

http://client:8082/getResHello

授权之后下面的表也有数据了

接下来我们看下客户端的scope

如果我们配置一个错误的scope,没有和授权服务器的一致

那么就会报下面的错误

我们对客户端的安全配置类改造

package com.example.client.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class SecurityConfig {


    /**
     *
     * 安全过滤器链
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //异常处理
        http.exceptionHandling()
                //访问拒绝处理程序
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                        response.setContentType("application/json;charset=utf-8");
                        response.getWriter().println("{code:403,msg:没有访问权限}");
                    }
                });

        //拦截所有请求
        http.authorizeRequests()
                //对某一个接口做scope权限控制 SCOPE_固定前缀+作用域
                .mvcMatchers("/getResHello").access("hasAuthority('SCOPE_write')")
                .anyRequest()
                .authenticated()
                .and()
                //使用oauth2登陆,自动跳转到授权服务的登陆界面
                .oauth2Login();
        return http.build();
    }
}

设置一个没有配置的作用域

再次访问

http://client:8082/getResHello

我们在客户端的yml加入作用域,一定要和授权服务器的作用域一样

我们在来访问一遍,一定要把这2个都勾选上,要不然还没有权限

接下来我们看下向access_token附加角色roles信息

现在是jwt解析的access_token是没有角色信息的

我们在授权服务器加一下,加入这部分代码,将roles写入jwt

package com.example.authserver.security;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
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.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Configuration
//开启web安全 应用在web环境下
// 1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略
// 2: 加载了AuthenticationConfiguration, 配置了认证信息
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private JdbcTemplate jdbcTemplate;




    /**
     *
     * 授权服务安全过滤器链
     * 第一个进来
     * @param
     * @return
     * @throws Exception
     */
    @Order(1)
    @Bean
    public SecurityFilterChain authorizedServiceSecurityFilterChain(HttpSecurity http) throws Exception {
        //授权服务配置 应用默认安全性 简化配置,在源码给你都配置好了
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.   //异常处理
                exceptionHandling().
                //设置默认的登陆界面
                authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"));
        return http.build();
    }

    /**
     * 默认安全过滤器链
     * 用于身份认证
     * 第二个进入
     * @param
     * @return
     * @throws Exception
     */
    @Order(2)
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        //必须认证才能登陆
        http.authorizeHttpRequests()
                .anyRequest()
                .authenticated()
                .and()
                //采用表单认证方式登陆
                .formLogin(Customizer.withDefaults());
        return http.build();
    }


    /**
     *
     * 配置用户信息 从数据库查询 实现用户的crud操作
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public UserDetailsService userDetailsService(){
        return new JdbcUserDetailsManager(dataSource);
    }





    /**
     *
     *  用于第三方认证
     *  主要管理第三方的客户端
     *  已注册客户端存储库
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(){
        //从数据库查询已经注册的客户端信息
        return new JdbcRegisteredClientRepository(jdbcTemplate);
//        RegisteredClient registeredClient=RegisteredClient.withId(UUID.randomUUID().toString())
//                //客户端id
//                .clientId("showDoc")
//                //{noop}不加密,客户端密码
//                .clientSecret("{noop}123456")
//                //客户端认证方法  客户端安全基本 客户端通过客户端密码认证方式接入
//                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//                //客户端允许使用的授权模式,授权类型,授权码模式,刷新token,客户端模式
//                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
//                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//                //客户端允许跳转的uri注册地址,回调地址 登陆成功之后跳转的地址
//                .redirectUri("http://auth-server:8080/authorized")
//                .redirectUri("http://client:8082/login/oauth2/code/test")
//                //客户端允许使用的范围授权 如果对方写的不对 那么就没有权限进行登陆
//                .scope("read")
//                .scope("write")
//                //是否开启用户手动确认,false为自动确认 就是在授权界面 是否选择 复选框
//                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
//                .build();
//        //内存注册客户端存储  不查询数据库
//        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    /**
     *
     * 通过非对称加密生成access_token(jwt)的签名部分
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public JWKSource<SecurityContext>jwkSource(){
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * ⽣成秘钥对,为jwkSource提供服务,私钥服务器⾃身持有,公钥对外开放。
     *
     * @return
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     *
     * 提供程序设置 默认配置
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public ProviderSettings providerSettings(){
        return ProviderSettings.builder().build();
    }

    /**
     *
     * 对应 oauth2_authorization表 授权服务
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public OAuth2AuthorizationService auth2AuthorizationService(){
        return new JdbcOAuth2AuthorizationService(jdbcTemplate,registeredClientRepository());
    }

    /**
     * 对应oauth2_authorization_consent表
     * 用户确认授权同意书
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public OAuth2AuthorizationConsentService auth2AuthorizationConsentService(){
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate,registeredClientRepository());
    }

    /**
     *
     * jwt编码上下文oauth2令牌自定义程序
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext>jwtEncodingContextOAuth2TokenCustomizer(){
        return context -> {
          //判断jwt的类型是否是access_token
          if(context.getTokenType()== OAuth2TokenType.ACCESS_TOKEN){
                //获得认证对象,当前用户信息
              Authentication principal = context.getPrincipal();
              List<String>roles=new ArrayList<>();
              //得到该用户的权限信息 role角色 放入集合
              for (GrantedAuthority authority : principal.getAuthorities()) {
                  roles.add(authority.getAuthority());
              }
              //写入jwt
              context.getClaims().claim("roles",roles);
          }

        };
    }
}

然后再资源服务器加入角色转换器,解析jwt中传过来的角色信息

package com.example.resserver.config;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 *
 * 角色转换器
 * @param
 * @return
 * @throws Exception
 */
public class RoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        //从jwt拿到角色信息
        List<String> roles = (List<String>) jwt.getClaims().get("roles");
        if(CollectionUtils.isEmpty(roles)){
            return new ArrayList<>();
        }
        List<GrantedAuthority>list=new ArrayList<>();
        for (String x : roles) {
            //把角色放入授予的权限集合中
            list.add(new SimpleGrantedAuthority(x));
        }
        return list;
    }
}

然后再资源服务器,修改安全配置类,使得角色转换器生效,并加了一个角色的权限

package com.example.resserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Configuration
public class SecurityConfig {


    /**
     *
     * 安全过滤器链
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //异常处理
        http.exceptionHandling()
                //访问拒绝处理程序
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                        response.setContentType("application/json;charset=utf-8");
                        response.getWriter().println("{code:403,msg:没有访问权限}");
                    }
                });
        //启用角色转换器解析roles 并完成授权      jwt身份验证转换器
        JwtAuthenticationConverter jwtAuthenticationConverter=new JwtAuthenticationConverter();
        //设置jwt授权机构转换为角色转换器
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new RoleConverter());

        //拦截所有请求
        http.authorizeRequests()
                    //测试转换器解析的角色 是否有这个权限
                    .mvcMatchers("/hello").access("hasRole('USER')")
                    .anyRequest()
                    .authenticated()
                    .and()
                    //oauth2资源服务器 使用jwt 带着jwt的token访问资源服务器
                    .oauth2ResourceServer().jwt()
                    //使转换器生效
                    .jwtAuthenticationConverter(jwtAuthenticationConverter);
            return http.build();
    }

}

当我们在jwt中加了角色之后,可以看到这里已经显示出来了

我们在访问这个接口,看看能不能访问

http://client:8082/getResHello

我们现在是有权限,可以访问

我们设置一个没有权限了,看看是啥效果,把USER改成USER1

可以看到没有权限访问

接下来我们在看下RBAC角色权限控制 

用户和角色是多对多的关系

角色和权限是绑定的

在资源服务器加入这些依赖

 <!-- 添加MyBatisPlus的依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- MySQL数据 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- druid  连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.14</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

修改资源服务器配置文件

server:
  port: 8081
logging:
  level:
    root: debug
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          #获取公钥路径 用于重新拿着公钥和base64加密出来的签名  和传过来的签名进行对比 防止篡改 这样保证了数据的安全
          jwk-set-uri: http://auth-server:8080/oauth2/jwks
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://192.168.23.131:13306/db3?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml



在资源服务器加入自定义决策器

package com.example.resserver.config;

import com.example.resserver.entity.RolePerVo;
import com.example.resserver.mapper.RolePerMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 *
 * 角色投票 决策器
 * @param
 * @return
 * @throws Exception
 */
@Slf4j
@Component
public class MyRoleVoter implements AccessDecisionVoter<Object> {

    @Autowired
    private RolePerMapper rolePerMapper;

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    /**
     *
     * 选票
     * @param
     * @return
     * @throws Exception
     */
    @Override
    public int vote(Authentication authentication, Object object,
                    Collection<ConfigAttribute> attributes) {
        //如果用户还没有认证,则直接拒绝
        if(authentication==null){
            //-1 表示拒绝访问
            return -1;
        }
        //获取已经授权过的角色对象
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        //采用ant语法规则的匹配器
        AntPathMatcher antPathMatcher=new AntPathMatcher();
        String requestUri=((FilterInvocation)object).getRequest().getRequestURI();
        //TODO 我这里查询的所有的角色权限,你可以改成根据当前用户获取对应的角色权限信息
        List<RolePerVo> list = rolePerMapper.getList();
        log.info("角色权限信息:{}",list);
        log.info("接口路径uri:{}",requestUri);
        for (RolePerVo rolePerVo : list) {
            //判断这个接口,在没在数据库中,这个人有没有这个接口的权限
            //注意这里匹配的是资源服务器的路径,不是你浏览器那个路径 是浏览器远程访问到资源服务器的那个接口
            if(antPathMatcher.match(rolePerVo.getUrl(),requestUri)){
                log.info("当前路径匹配,可以往下执行");
                for (GrantedAuthority authority : authorities) {
                    //因为角色code 在spring oauth2中 固定以ROLE_开头,所以要拼接 才能匹配上
                    String roleName="role_"+rolePerVo.getRoleCode().toLowerCase();
                    if(roleName.equals(authority.getAuthority().toLowerCase())){
                        //因为传过来的数据是大写的,所以要转换成小写 来匹配
                        //有一个角色匹配上了 那么就放行
                        //1表示 访问授权的
                        return 1;
                    }
                }
            }
        }
        log.info("没有访问权限");
        //-1 表示拒绝访问
        return -1;
    }
}

在资源服务器修改安全配置类,加入决策器生效

package com.example.resserver.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.expression.WebExpressionVoter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 * 安全配置类
 * @param
 * @return
 * @throws Exception
 */
@Configuration
public class SecurityConfig {

    @Autowired
    private MyRoleVoter myRoleVoter;

    /**
     *
     * 安全过滤器链
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //异常处理
        http.exceptionHandling()
                //访问拒绝处理程序
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                        //返回json格式
                        response.setContentType("application/json;charset=utf-8");
                        //没有登陆 直接访问其他接口 就报401
                        response.setStatus(403);
                        Map<String,Object> map=new HashMap<>();
                        map.put("code",403);
                        map.put("msg","没有访问权限");
                        ObjectMapper objectMapper=new ObjectMapper();
                        String s=objectMapper.writeValueAsString(map);
                        //把json数据 写入 返回给前端
                        PrintWriter writer=response.getWriter();
                        writer.write(s);
                        writer.flush();
                        writer.close();
                    }
                });
        //启用角色转换器解析roles 并完成授权      jwt身份验证转换器
        JwtAuthenticationConverter jwtAuthenticationConverter=new JwtAuthenticationConverter();
        //设置jwt授权机构转换为角色转换器
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new RoleConverter());

        //使决策器生效 拦截所有请求 动态匹配数据库的 角色权限信息
        http.authorizeRequests()
                .accessDecisionManager(accessDecisionManager())
                .anyRequest().authenticated();

        //oauth2资源服务器 使用jwt 带着jwt的token访问资源服务器
        http.oauth2ResourceServer().jwt()
                    //使转换器生效 解析jwt里面的roles 角色信息
                    .jwtAuthenticationConverter(jwtAuthenticationConverter);
            return http.build();
    }

    /**
     * 实例化决策器对象
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public AccessDecisionManager accessDecisionManager(){
        List<AccessDecisionVoter<? extends Object>> decisionVoters=
                new ArrayList<>();
        //固定写法 网络表达投票器
        decisionVoters.add(new WebExpressionVoter());
        //传入自定义的决策器 投票器
        decisionVoters.add(myRoleVoter);
        //固定写法 经过身份验证的投票者
        decisionVoters.add(new AuthenticatedVoter());
        //基于一致性
        return new UnanimousBased(decisionVoters);
    }

}

核心在这部分

创建角色表,权限表,角色权限关联表

CREATE TABLE `sys_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) DEFAULT NULL COMMENT '权限路径',
  `per_name` varchar(255) DEFAULT NULL COMMENT '权限名称',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='权限表';


INSERT INTO `sys_permission` (`id`, `url`, `per_name`) VALUES (1, '/hello', '你好接口');
INSERT INTO `sys_permission` (`id`, `url`, `per_name`) VALUES (2, '/getUser', '获取用户解决');
INSERT INTO `sys_permission` (`id`, `url`, `per_name`) VALUES (3, '/admin', '获取管理员接口');

CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(255) DEFAULT NULL COMMENT '角色名称',
  `role_code` varchar(255) DEFAULT NULL COMMENT '角色编码',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='角色表';


INSERT INTO `sys_role` (`id`, `role_name`, `role_code`) VALUES (1, '普通员工', 'USER');
INSERT INTO `sys_role` (`id`, `role_name`, `role_code`) VALUES (2, '管理员', 'ADMIN');
CREATE TABLE `sys_role_per` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role_id` int(11) DEFAULT NULL COMMENT '角色id',
  `per_id` int(11) DEFAULT NULL COMMENT '权限id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='角色权限关联表';


INSERT INTO `sys_role_per` (`id`, `role_id`, `per_id`) VALUES (1, 1, 1);
INSERT INTO `sys_role_per` (`id`, `role_id`, `per_id`) VALUES (2, 2, 2);
INSERT INTO `sys_role_per` (`id`, `role_id`, `per_id`) VALUES (3, 2, 3);
INSERT INTO `sys_role_per` (`id`, `role_id`, `per_id`) VALUES (4, 1, 2);

我们改造一下客户端的控制层

package com.example.client.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

@Slf4j
@RestController
public class ClientControler {

    @Autowired
    private RestTemplate restTemplate;

    /**
     *
     * 获取资源服务器的接口
     *
     * RegisteredOAuth2AuthorizedClient 注册在授权服务器的客户端
     * @param
     * @return
     * @throws Exception
     */
    @GetMapping("/getResHello")
    public String getResUser(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client){
        HttpHeaders headers=new HttpHeaders();
        //从授权客户端拿到token
        String token=client.getAccessToken().getTokenValue();
        log.info("令牌为:{}",token);
        //把token 放入请求头中 ,token类型为Bearer
        //在源码里面this.set("Authorization", "Bearer " + token) 自动拼接Bearer空格
        headers.setBearerAuth(token);
        HttpEntity<String>request=new HttpEntity<>("",headers);

        try {
            //拿着令牌去获取资源服务器的接口
            ResponseEntity<String>responseEntity=restTemplate
                    .exchange("http://res-server:8081/hello",
                            HttpMethod.GET,request,String.class);
            log.info("============",responseEntity);
            String body=responseEntity.getBody();
            return body;
        }catch (HttpClientErrorException e){
            return e.getMessage();
        }
    }
}

3个服务都启动

我们在访问有权限的情况

http://client:8082/getResHello

 我们把数据权限删除,模拟没权限的场景

把这个4,1删除掉,在次访问

 

 接下来我们看下刷新token

回到授权服务器初始化客户端哪里

package com.example.authserver;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.util.UUID;

@SpringBootTest
class AuthServerApplicationTests {
        @Autowired
        private UserDetailsManager userDetailsManager;
        @Autowired
        private RegisteredClientRepository registeredClientRepository;




        /**
         *
         * 创建用户
         * @param
         * @return
         * @throws Exception
         */
        @Test
        public void createUser(){
            //添加到用户表,和用户权限表中
            UserDetails userDetails= User.builder().passwordEncoder(
                    x->"{bcrypt}"+new BCryptPasswordEncoder().encode(x))
                    .username("zhangsan1")
                    .password("123456")
                    .roles("USER")
                    .build();
            userDetailsManager.createUser(userDetails);
        }

        /**
         *
         * 注册客户端
         * @param
         * @return
         * @throws Exception
         */
        @Test
        public void registerClient(){
            RegisteredClient registeredClient=RegisteredClient.withId(UUID.randomUUID().toString())
                    //客户端id
                    .clientId("showDoc")
                    //客户端密码 加密
                    .clientSecret("{bcrypt}"+new BCryptPasswordEncoder().encode("123456"))
                    //客户端认证方法  客户端安全基本 客户端通过客户端密码认证方式接入
                    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                    //客户端允许使用的授权模式,授权类型,授权码模式,刷新token,客户端模式
                    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                    //如果要刷新token 必须要有这个选择
                    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                    .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                    //客户端允许跳转的uri注册地址,回调地址 登陆成功之后跳转的地址
                    .redirectUri("http://auth-server:8080/authorized")
                    .redirectUri("http://client:8082/login/oauth2/code/test")
                    //客户端允许使用的范围授权 如果对方写的不对 那么就没有权限进行登陆
                    .scope("read")
                    .scope("write")
                    //是否开启用户手动确认,false为自动确认 就是在授权界面 是否选择 复选框
                    .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                    //设置access_token
                    .tokenSettings(TokenSettings.builder()
                            //accessToken 生存时间
                            .accessTokenTimeToLive(Duration.ofSeconds(10))
                            //刷新token 生存时间
                            .refreshTokenTimeToLive(Duration.ofSeconds(15))
                            .build())
                    .build();
            //保存到oauth2_registered_client表里
            registeredClientRepository.save(registeredClient);
        }

    }



 先把数据库的3张数据删除了,在初始化

 存在数据库这个字段中

 我们再次访问

 当过期时间到了之后,又再次请求了一次授权,这就是刷新token

 设置多长时间过期由你来控制

 

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值