SpringBoot+security+oauth2搭建demo

前言

搭建Spring Security OAuth2密码模式demo使用postman测试,一方面在慢慢完善使用oauth2过程中做一个系统的总结,一方面做一个知识输出

一、认证授权服务搭建

  • 添加依赖
<properties>
    <java.version>1.8</java.version>
    <spring.boot.version>2.3.12.RELEASE</spring.boot.version>
    <spring-cloud-starter-oauth2.version>2.2.5.RELEASE</spring-cloud-starter-oauth2.version>
    <slf4j.version>1.7.21</slf4j.version>
</properties>

<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

   <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-oauth2</artifactId>
       <version>${spring-cloud-starter-oauth2.version}</version>
   </dependency>

   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.8</version>
   </dependency>

   <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>${slf4j.version}</version>
   </dependency>

   <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
       <version>5.1.37</version>
   </dependency>

   <dependency>
       <groupId>cn.hutool</groupId>
       <artifactId>hutool-all</artifactId>
       <version>5.8.4.M1</version>
   </dependency>

   <!--jwt-->
   <dependency>
       <groupId>io.jsonwebtoken</groupId>
       <artifactId>jjwt</artifactId>
       <version>0.9.1</version>
   </dependency>

   <!--jdbc-->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-jdbc</artifactId>
   </dependency>

   <!--redis-->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
   </dependency>

</dependencies>
  • AuthorizationServerConfig.java
/**
 * @Author LY
 * @ClassName AuthorizationServerConfig
 * @Date 2023/6/10 14:42
 * @Description 认证服务器配置
 * @Version 1.0
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private UserServiceImpl userDetailsService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 初始化 redisTokenStore 用户将token 放入redis
     * @return
     */
    @Bean
    public RedisTokenStore redisTokenStore(){
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        redisTokenStore.setPrefix("TOKEN:");
        return redisTokenStore;
    }

    /**
     * 设置认证令牌放行
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
            //允许所有资源服务器访问公钥端点(/oauth/token_key)
            //只允许验证用户访问令牌解析端点(/oauth/check_token)
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("permitAll()")
            //允许表单验证
            .allowFormAuthenticationForClients();//主要是让/oauth/token支持client_id以及client_secret作登录认证
    }

    /**
     * 配置授权以及令牌的访问端点和令牌服务
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //认证信息从数据库获取
        clients.withClientDetails(clientDetails());
//        // 使用内存操作测试
//        clients.inMemory()
//                .withClient("client-app")
//                .secret(new BCryptPasswordEncoder().encode("123456"))
//                .resourceIds("resId")
//                .scopes("all")
//                .authorizedGrantTypes("password", "refresh_token", "client_credentials")
//                .and()
//                .withClient("client-app2")
//                .secret(new BCryptPasswordEncoder().encode("123456"))
//                .resourceIds("resId2")
//                .scopes("all")
//                .authorizedGrantTypes("password", "refresh_token", "client_credentials")
//                .accessTokenValiditySeconds(3600)
//                .refreshTokenValiditySeconds(86400);
    }

    @Bean
    public ClientDetailsService clientDetails() {
    		// 自定义实现
        return new CustomJdbcClientDetailsService(dataSource);
        // 默认实现
//        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 配置令牌端点的安全约束
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                // 请求方式
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                // 指定认证管理器
                .authenticationManager(authenticationManager)
                // 用户账号密码认证
                .userDetailsService(userDetailsService)
//                .accessTokenConverter(jwtAccessTokenConverter())
                // 指定token存储位置
                .tokenStore(redisTokenStore())
                // 令牌增强对象 , 增强返回的结果
                .tokenEnhancer((accessToken, authentication) -> {
                    // 获取用户信息,然后设置
                    SecurityUser user = (SecurityUser) authentication.getPrincipal();
                    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
                    map.put("userId",user.getId());
                    map.put("usernmae",user.getUsername());
                    map.put("enabled", user.getEnabled());
                    DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
                    token.setAdditionalInformation(map);
                    return token;
                });
    }
	/*------------使用jwt配置token-------------*/
//    @Bean
//    public TokenStore jwtTokenStore(){
//        JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());
//        return jwtTokenStore;
//    }
//
//    @Bean
//    public JwtAccessTokenConverter jwtAccessTokenConverter(){
//        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//        jwtAccessTokenConverter.setSigningKey("SigningKey");//设置对称签名秘钥
//        return jwtAccessTokenConverter;
//    }
}
  • ResourceServerConfig.java
/**
 * @Author LY
 * @ClassName ResourceServerConfig
 * @Date 2023/6/10 22:27
 * @Description 资源服务器配置
 * @Version 1.0
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 配置资源id,这里的资源id和授权服务器中的资源id一致
        resources.resourceId("resId")
                // 设置这些资源仅基于令牌认证
                .stateless(true);
    }

    /**
     * 配置 URL 访问权限
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}
  • WebSecurityConfig.java
/**
 * @Author LY
 * @ClassName WebSecurityConfig
 * @Date 2023/6/10 14:52
 * @Description SpringSecurity授权配置
 * @Version 1.0
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private UserServiceImpl userDetailsService;

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	// 从数据库读取配置
        auth.userDetailsService(userDetailsService);
        // 使用内存操作
//        auth
//            .inMemoryAuthentication()
//            .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN")
//            .and()
//            .withUser("andy").password(new BCryptPasswordEncoder().encode("123456")).roles("TEST");
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 拦截配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()//配置需要认证的请求
                .antMatchers("/oauth/**").permitAll() //放行
                .anyRequest().authenticated()//其他请求需要认证
                .and().csrf().disable()
                // 禁用session,因为我们使用的是token,session没有用途
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

核心配置基本完成,下面配置自定义UserDetails,UserDetailsService,JdbcClientDetailsService

  • UserDetails自定义 这个类也是从网上摘取的,可自行进行扩展改造
/**
 * @Author LY
 * @ClassName SecurityUser
 * @Date 2023/6/10 14:58
 * @Description Security用户信息
 * @Version 1.0
 */
@Data
public class SecurityUser implements UserDetails {

    /**
     * ID
     */
    private Long id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 用户密码
     */
    private String password;
    /**
     * 用户状态
     */
    private Boolean enabled;
    /**
     * 权限数据
     */
    private Collection<SimpleGrantedAuthority> authorities;

    public SecurityUser() {}

    public SecurityUser(UserDTO userDTO) {
        this.setId(userDTO.getId());
        this.setUsername(userDTO.getUsername());
        this.setPassword(userDTO.getPassword());
        this.setEnabled(userDTO.getStatus() == 1);
        if (userDTO.getRoles() != null) {
            authorities = new ArrayList<>();
            userDTO.getRoles().forEach(item -> authorities.add(new SimpleGrantedAuthority(item)));
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

/**
 * @Author LY
 * @ClassName UserDTO
 * @Date 2023/6/10 14:56
 * @Description 用户信息
 * @Version 1.0
 */
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
public class UserDTO{
    private Long id;
    private String username;
    private String password;
    private Integer status;
    private List<String> roles;

}
  • UserDetailsService自定义
/**
 * @Author LY
 * @ClassName UserServiceImpl
 * @Date 2023/6/10 14:54
 * @Description 用户信息校验
 * @Version 1.0
 */
@Service
@Slf4j
public class UserServiceImpl implements UserDetailsService {

    private List<UserDTO> userList;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    public void initData() {
        String password = passwordEncoder.encode("123456");
        userList = new ArrayList<>();
        userList.add(new UserDTO(1L,"admin", password,1, CollUtil.toList("ADMIN")));
        userList.add(new UserDTO(2L,"andy", password,1, CollUtil.toList("TEST")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("当前登录用户:{}",username);
        // 这里使用上面定义的用户及权限认证,可以换成自己的从数据库读取
        List<UserDTO> findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
        if (CollUtil.isEmpty(findUserList)) {
            throw new UsernameNotFoundException("用户名或密码错误!");
        }
        SecurityUser securityUser = new SecurityUser(findUserList.get(0));
        if (!securityUser.isEnabled()) {
            throw new DisabledException("该账户已被禁用,请联系管理员!");
        } else if (!securityUser.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定,请联系管理员!");
        } else if (!securityUser.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期,请联系管理员!");
        } else if (!securityUser.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录!");
        }
//        String password = passwordEncoder.encode("123456");
//        CustomUserDetails user = new CustomUserDetails(username,password, AuthorityUtils.
//                commaSeparatedStringToAuthorityList("ROLE_ADMIN"));
//        return user;
        return securityUser;
    }
}
  • JdbcClientDetailsService自定义
/**
 * @Author LY
 * @create 2023/6/7 11:17
 * @Description 自定义jdbc数据库读取信息
 */
@Slf4j
public class CustomJdbcClientDetailsService extends JdbcClientDetailsService {

    //操作oauth_client_details数据库SQL语句,另外可以自行注入
    private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, "
            + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
            + "refresh_token_validity, additional_information, autoapprove";
    private static final String CLIENT_FIELDS = "client_secret, " + CLIENT_FIELDS_FOR_UPDATE;
    private static final String BASE_FIND_STATEMENT = "select client_id, " + CLIENT_FIELDS + " from oauth_client_details";
    private static final String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
    private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
        private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (" + CLIENT_FIELDS
            + ", client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
    private static final String DEFAULT_UPDATE_STATEMENT = "update oauth_client_details " + "set "
            + CLIENT_FIELDS_FOR_UPDATE.replaceAll(", ", "=?, ") + "=? where client_id = ?";
    private static final String DEFAULT_UPDATE_SECRET_STATEMENT = "update oauth_client_details "
            + "set client_secret = ? where client_id = ?";
    private static final String DEFAULT_DELETE_STATEMENT = "delete from oauth_client_details where client_id = ?";

    //1.用于client_secret密码入库与出库时转化
    private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
    //2.数据库存操作。
    private final JdbcTemplate jdbcTemplate;
    private JdbcListFactory listFactory;

    public CustomJdbcClientDetailsService(DataSource dataSource) {
        super(dataSource);
        Assert.notNull(dataSource, "DataSource required");
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(jdbcTemplate));
    }

    /**
     * 核心方法。加载ClientDetails by clientId
     */
    @Override
    public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
        log.info("==== 加载ClientDetails by clientId:{} ====",clientId);
        ClientDetails details = null;
        try {
            details = jdbcTemplate.queryForObject(DEFAULT_SELECT_STATEMENT, new ClientDetailsRowMapper(), clientId);
        }
        catch (EmptyResultDataAccessException e) {
            throw new NoSuchClientException("No client with requested id: " + clientId);
        }
        return details;
    }
		
		// 重新映射字段显示   需要哪些字段添加哪些
    static class ClientDetailsRowMapper implements RowMapper<ClientDetails> {

        @Override
        public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
            // 筛选值
            BaseClientDetails details = new BaseClientDetails();
            details.setClientId(rs.getString("client_id"));
            details.setClientSecret(rs.getString("client_secret"));
            details.setResourceIds(StringUtils.commaDelimitedListToSet(rs.getString("resource_ids")));
            details.setScope(StringUtils.commaDelimitedListToSet(rs.getString("scope")));
            details.setAuthorizedGrantTypes(StringUtils.commaDelimitedListToSet(rs.getString("authorized_grant_types")));
            details.setAuthorities(getAuthority(rs.getString("authorities")));
            return details;
        }

        private Collection<? extends GrantedAuthority> getAuthority(String authorities) {
            if (!StringUtils.hasText(authorities)) {
                return new ArrayList<>();
            }
            return AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
        }
    }
}
  • yml配置
server:
  port: 10001

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/permission
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver

  # redis 配置
  redis:
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 密码
    password:

logging:
  level:
    org:
      springframework:
      	# 打印security日志
        security: debug

测试认证:http://localhost:10001/oauth/token
grant_type:password
client_id:client-app
client_secret:123456
username:admin
password:123456
在这里插入图片描述
可以看到成功获取了access_token
测试授权我们先需要添加测试控制层AuthController.java

@RestController
public class AuthController {
		// 需要ADMIN权限访问
    @PreAuthorize("hasAuthority('ADMIN')")
    @RequestMapping(value = "/getStr1",method = {RequestMethod.GET})
    public String getStr1(){
        return "getStr1";
    }
		// 需要TEST权限访问
    @PreAuthorize("hasAuthority('TEST')")
    @RequestMapping(value = "/getStr2",method = {RequestMethod.GET})
    public String getStr2(){
        return "getStr2";
    }

    @RequestMapping(value = "/getStr3",method = {RequestMethod.GET})
    public String getStr3(){
        return "getStr3";
    }
}

授权测试

测试地址:http://localhost:10001/getStr1
在这里插入图片描述测试地址:http://localhost:10001/getStr2
在这里插入图片描述可以看到访问getStr1能正常访问,访问getStr2提示“不允许访问”,因为我们当前登录的账号只有ADMIN权限,测试结果复核预期

资源服务id测试

从上面的配置我们指定认证授权服务配置的client_id=client-app属于resourceId=resId,另一个client_id=client-app2属于resourceId=resId2
在这里插入图片描述在这里插入图片描述可以看到当前登录的用户属于resourceId=resId资源并没有其他资源访问权限,测试期望预期,认证授权服务端测试结束

我们可以看下redis存储的TOKEN信息:
在这里插入图片描述

二、资源服务搭建

<properties>
  <java.version>1.8</java.version>
  <spring.boot.version>2.3.12.RELEASE</spring.boot.version>
  <spring-cloud-starter-oauth2.version>2.2.5.RELEASE</spring-cloud-starter-oauth2.version>
  <slf4j.version>1.7.21</slf4j.version>
</properties>

<dependencies>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
      <version>${spring-cloud-starter-oauth2.version}</version>
  </dependency>

  <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.8</version>
  </dependency>

  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.version}</version>
  </dependency>

  <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>5.8.4.M1</version>
  </dependency>
  
</dependencies>
  • ResourceServerConfigurerAdapter.java
/**
 * @Author LY
 * @ClassName WebSecurityConfig
 * @Date 2023/6/10 17:58
 * @Description 资源服务配置
 * @Version 1.0
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 配置资源id,这里的资源id和授权服务器中的资源id一致
        resources.resourceId("resId2")
                .stateless(true);
    }

    /**
     * 配置 URL 访问权限
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/oauth/**").permitAll() //设置/oauth/**的接口不需要授权就可以访问
                .anyRequest().authenticated();
    }

}
  • yml配置
server:
  port: 10002

#spring:
#  datasource:
#    url: jdbc:mysql://127.0.0.1:3306/permission
#    username: root
#    password: root
#    driver-class-name: com.mysql.jdbc.Driver

security:
  oauth2:
    client:
      # 客户端id
      client-id: client-app2
      # 客户端密钥
      client-secret: 123456
#      resource-ids: resId2
      access-token-uri: http://127.0.0.1:10001/oauth/token
      user-authorization-uri: http://127.0.0.1:10001/oauth/authorize
    resource:
      # 认证服务器验证token的请求接口
      token-info-uri: http://127.0.0.1:10001/oauth/check_token
      # 获取用户信息
      user-info-uri: http://127.0.0.1:10001/users/current

logging:
  level:
    org:
      springframework:
      	# security日志打印
        security: debug
  • 控制层
/**
 * @Author LY
 * @ClassName SystemController
 * @Date 2023/6/10 17:56
 * @Description 测试资源服务接口权限
 * @Version 1.0
 */
@RestController
public class SystemController {

    @PreAuthorize("hasAuthority('ADMIN')")
    @RequestMapping(value = "/getSys1",method = {RequestMethod.GET})
    public String getSys1(){
        final SecurityContext context = SecurityContextHolder.getContext();
        System.out.println(context.getAuthentication());

        return "sys1";
    }

    @PreAuthorize("hasAuthority('TEST')")
    @RequestMapping(value = "/getSys2",method = {RequestMethod.GET})
    public String getSys2(){
        return "sys2";
    }

    @RequestMapping(value = "/getSys3",method = {RequestMethod.GET})
    public String getSys3(){
    		// 登录信息
        final SecurityContext context = SecurityContextHolder.getContext();
        System.out.println(context.getAuthentication().isAuthenticated());
        System.out.println(context.getAuthentication().getAuthorities());
        System.out.println(context.getAuthentication().getPrincipal());
        System.out.println(context.getAuthentication().getDetails());
        return "sys3";
    }

}

资源服务搭建完成,接下来使用postman测试接口:
在这里插入图片描述在这里插入图片描述在这里插入图片描述可以看见当前登录的用户是存在ADMIN权限而没有TEST权限,测试期望预期
接下来测试资源服务resourceId验证:
在这里插入图片描述在这里插入图片描述可以看到当前登录的用户属于resourceId=resId2资源并没有其他资源访问权限,测试期望预期,资源服务端测试访问资源id验证结束

总结: 整个认证授权测试结束,其中测试了使用注解限制,测试了resourceId资源服务限制,测试结果基本符合我们的期望,到此使用SpringBoot+security+oauth2搭建demo完成

源代码地址:https://gitee.com/xiaomao12/ly_oauth2.git

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值