Kehaw

基于Spring Security + OAuth2 + JWT 的权限认证(一)


整合 Spring Security 、 OAuth2 以及 JWT 做权限认证,使用 Consul 作为注册中心,数据库是 MySQL,ORM框架是 JPA,版本号详细参考POM文件。

本篇将会把认证服务器和资源服务器放在一起,当然也不必担心逻辑混乱,我会详细介绍,并且这两个可以很方便的拆开。

不要用在正式环境,本教程只是让你能在本地环境运行

配置相关

POM文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.banksteel</groupId>
    <artifactId>auth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR7</spring-cloud.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</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</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>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

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

    <repositories>
        <repository>
            <id>central</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <layout>default</layout>
            <!-- 是否开启发布版构件下载 -->
            <releases>
                <enabled>true</enabled>
            </releases>
            <!-- 是否开启快照版构件下载 -->
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

YML文件

server:
  port: 8080
management:
  endpoint:
    health:
      show-details: always
auth:
  exclude-paths:
    - /oauth/**
    - /actuator/health
    - /login
spring:
  application:
    name: auth
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ****
    hikari:
      maximumPoolSize: 20
      max-lifetime: 570000
      connection-test-query: select 1
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://localhost:3306/auth?autoReconnect=true&serverTimezone=UTC
  cloud:
    consul:
      host: 192.168.200.81
      discovery:
        service-name: ${spring.application.name}
        instance-id: ${spring.application.name}
        prefer-ip-address: true

YML文件中,auth.exclude-paths是我自定义的属性,用来标记哪些 URI 不用经过权限控制。

初始化 OAuth2.0 所需要的数据库表

这里需要注意,如果你使用的是 MySQL 的话,将 LONGVARBINARY 改成 BLOB 类型。

-- used in tests that use HSQL
create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication LONGVARBINARY,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication LONGVARBINARY
);

create table oauth_code (
  code VARCHAR(256), authentication LONGVARBINARY
);

create table oauth_approvals (
  userId VARCHAR(256),
  clientId VARCHAR(256),
  scope VARCHAR(256),
  status VARCHAR(10),
  expiresAt TIMESTAMP,
  lastModifiedAt TIMESTAMP
);


-- customized oauth_client_details table
create table ClientDetails (
  appId VARCHAR(256) PRIMARY KEY,
  resourceIds VARCHAR(256),
  appSecret VARCHAR(256),
  scope VARCHAR(256),
  grantTypes VARCHAR(256),
  redirectUrl VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(256)
);

然后初始化我们自己的数据库:

CREATE TABLE `auth_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(45) NOT NULL,
  `password` varchar(64) NOT NULL,
  `name` varchar(45) NOT NULL,
  `enabled` tinyint unsigned DEFAULT '1',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username_UNIQUE` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'

CREATE TABLE `auth_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(45) NOT NULL,
  `enabled` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name_UNIQUE` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表'

CREATE TABLE `auth_user_role` (
  `user_id` bigint NOT NULL,
  `role_id` bigint NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表'

插入数据:

LOCK TABLES `auth_role` WRITE;
/*!40000 ALTER TABLE `auth_role` DISABLE KEYS */;
INSERT INTO `auth_role` VALUES (1,'管理员',1);
/*!40000 ALTER TABLE `auth_role` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping data for table `auth_user`
--

LOCK TABLES `auth_user` WRITE;
/*!40000 ALTER TABLE `auth_user` DISABLE KEYS */;
INSERT INTO `auth_user` VALUES (1,'admin','$2a$10$oTnIFFRMMFA/OpCQQnuz1eFsHBpHSNj.aF5BXPME2I3b9RdfsXjSG','葛昊',1);
/*!40000 ALTER TABLE `auth_user` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping data for table `auth_user_role`
--

LOCK TABLES `auth_user_role` WRITE;
/*!40000 ALTER TABLE `auth_user_role` DISABLE KEYS */;
INSERT INTO `auth_user_role` VALUES (1,1);
/*!40000 ALTER TABLE `auth_user_role` ENABLE KEYS */;
UNLOCK TABLES;

LOCK TABLES `oauth_client_details` WRITE;
/*!40000 ALTER TABLE `oauth_client_details` DISABLE KEYS */;
INSERT INTO `oauth_client_details` VALUES ('main-gateway',NULL,'$2a$10$u87bph9ANQccDD6XRLfbluxnLL.jdc5/oGv7iMsMvKZD9f23JAad6','all','password',NULL,NULL,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `oauth_client_details` ENABLE KEYS */;
UNLOCK TABLES;

两个加密的密码都是123

生成证书公钥和私钥

这里需要注意的是,OAuth2.0 只认 Open SSL 的证书。

keytool -genkeypair -alias {别名} -keyalg RSA -keypass {更改条目的密钥口令} -keystore {文件名}.jks -storepass {更改密钥库的存储口令}

生成完毕之后,通过以下命令得到公钥:

keytool -list -rfc --keystore {文件名}.jks | openssl x509 -inform pem -pubkey

输入 -keypass 参数后的密码即可得到一个公钥如下:

-----BEGIN PUBLIC KEY-----
//这里有一堆
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
//这里也有一堆
-----END CERTIFICATE-----

将第一段,也就是BEGIN PUBLIC KEY里的内容存储下来保存到 public.cert 文件中即可,其实就是一个以 .cert 结尾的 txt 文本文件,你用 .txt 保存也没问题。

两个文件都保存到 resource 文件夹下进行备用。

Java 代码

Spring Security OAuth2.0 的核心只有四个Java类,注意从现在开始要认真仔细的看了,描述性的文字一定不能遗漏、跳过。

启动类

启动类比较简单,没什么好说的。

@SpringCloudApplication
public class AuthApplication {

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

}

WebSecurityConfig

这个Java类会引出很多依赖的Java类,是 Spring Security OAuth2.0 的核心配置,先看代码:

package com.banksteel.auth.config;

import com.banksteel.auth.filter.UsernameRequestFilter;
import com.banksteel.auth.handler.LoginFailureHandler;
import com.banksteel.auth.handler.LoginSuccessHandler;
import com.banksteel.auth.handler.RequestAuthenticationEntryPoint;
import com.banksteel.auth.service.MyUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * 安全配置
 *
 * @author gehao
 * @since 2020年8月27日
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 连接数据库查询用户登录信息
     */
    @Resource
    private MyUserDetailsService myUserDetailsService;

    /**
     * 未授权的错误回调
     */
    @Resource
    private RequestAuthenticationEntryPoint requestAuthenticationEntryPoint;

    /**
     * 登录成功的回调
     */
    @Resource
    private LoginSuccessHandler loginSuccessHandler;

    /**
     * 登录失败的回调
     */
    @Resource
    private LoginFailureHandler loginFailureHandler;

    /**
     * 请求过滤器
     */
    @Resource
    private UsernameRequestFilter usernameRequestFilter;

    /**
     * 自定义的配置属性
     */
    @Resource
    private AuthProps authProps;


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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    /**
     * 使用数据库进行登录校验
     *
     * @return 校验器
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 设置userDetailsService
        provider.setUserDetailsService(myUserDetailsService);
        // 禁止隐藏用户未找到异常
        provider.setHideUserNotFoundExceptions(false);
        // 使用BCrypt进行密码的hash
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling().authenticationEntryPoint(requestAuthenticationEntryPoint)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(authProps.getExcludePaths().toArray(new String[0])).permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
                .and()
                .addFilterBefore(usernameRequestFilter, UsernamePasswordAuthenticationFilter.class);

        http
                .headers()
                .frameOptions().sameOrigin()
                .cacheControl();

    }

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

这段代码需要注意,如果你在同一个项目中,启用了 ResourceServer,也就是 @EnableResourceServer 那么,这段代码中与formLogin()相关的配置是不起作用的,具体看下面的代码注释:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .exceptionHandling().authenticationEntryPoint(requestAuthenticationEntryPoint)
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(authProps.getExcludePaths().toArray(new String[0])).permitAll()
            .anyRequest().authenticated()
            .and()
            //从这里开始
            .formLogin()
            .loginProcessingUrl("/login") 
            .successHandler(loginSuccessHandler) 
            .failureHandler(loginFailureHandler) 
            //到此为止,上面四行配置在 Resource Server 模式下是不起作用的
            .and()
            .addFilterBefore(usernameRequestFilter, UsernamePasswordAuthenticationFilter.class);

    http
            .headers()
            .frameOptions().sameOrigin()
            .cacheControl();

}

在启用 Resource Server 的前提下,访问 /login 将会返回 404 错误。

下面将会从本类中所涉及到的其他类逐步深入下去。

MyUserDetailsService
package com.banksteel.auth.service;

import com.banksteel.auth.dao.MyUserRepository;
import com.banksteel.auth.domain.dto.MyUserDetails;
import com.banksteel.auth.domain.entity.MyUser;
import com.banksteel.auth.domain.entity.Role;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @author gehao
 * @since 2020/8/26
 */
@Service
@Slf4j
public class MyUserDetailsService implements UserDetailsService {

    @Resource
    private MyUserRepository myUserRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        MyUser myUser = myUserRepository.findByUsername(username);
        if (myUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        log.info("获取用户的基本信息:" + myUser.getName());
        log.info("获取用户的角色信息:");
        for (Role role : myUser.getRoles()) {
            log.info(role.getName());
        }
        MyUserDetails myUserDetails = new MyUserDetails(myUser);
        return User.withUserDetails(myUserDetails).roles(myUser.getRoles().stream().map(Role::getName).toArray(String[]::new)).build();
    }
}

这里需要注意的是,一定要通过 .roles(...) 函数将角色信息传递给它,否则将不能得到角色信息从而访问被拒绝。

MyUserRepository
package com.banksteel.auth.dao;

import com.banksteel.auth.domain.entity.MyUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 用户查询
 * @author gehao
 * @since 2020年8月27日 13:55:02
 */
@Repository
public interface MyUserRepository extends JpaRepository<MyUser, Long> {

    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 用户信息
     */
    MyUser findByUsername(String username);
}

MyUserDetails
package com.banksteel.auth.domain.dto;

import com.banksteel.auth.domain.entity.MyUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.stream.Collectors;

/**
 * @author gehao
 * @since 2020/8/25
 */
public class MyUserDetails implements UserDetails {

    private final MyUser user;

    private final Collection<? extends GrantedAuthority> authorities;

    public MyUserDetails(MyUser user) {
        this.user = user;
        //注意这里,我是将 Role 作为 Authority 给设置进来了,实际不是这么用的
        this.authorities = user.getRoles().stream().map(item -> new SimpleGrantedAuthority(item.getName())).collect(Collectors.toList());
    }

    public MyUser getUser() {
        return user;
    }

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

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return this.user.isEnabled();
    }
}
MyUser
package com.banksteel.auth.domain.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
@Table(name = "auth_user", uniqueConstraints = {
        @UniqueConstraint(name = "username_UNIQUE", columnNames = {"username"})
})
public class MyUser implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 45)
    private String username;

    @Column(length = 64)
    private String password;

    @Column(length = 45)
    private String name;

    @Column(length = 1)
    private boolean enabled;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "auth_user_role", joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private List<Role> roles = new ArrayList<>();

}
Role
package com.banksteel.auth.domain.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @author gehao
 * @since 2020/8/24
 */
@Data
@Entity
@Table(name = "auth_role")
public class Role implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 45)
    private String name;

    @Column(length = 1)
    private Boolean enabled;

    @ManyToMany(mappedBy = "roles")
    private List<MyUser> myUsers = new ArrayList<>();
}

回调函数

回调函数包括:LoginSuccessHandlerLoginFailureHandlerRequestAuthenticationEntryPoint。因为在 Resource Server 模式下并不生效,因此这里只简单说一下他们各自实现了什么接口,你只需要在里面简单打印一下日志即可。

LoginSuccessHandler 实现 AuthenticationSuccessHandler 接口。

LoginFailureHandler 实现 AuthenticationFailureHandler 接口。

RequestAuthenticationEntryPoint 实现 AuthenticationEntryPoint 接口。

UsernameRequestFilter

我们只在此处进行一个简单的打印,实际上权限的具体控制都可以在这里进行开发,也可以做多个过滤器,具体看你的代码设计。

package com.banksteel.auth.filter;

import com.banksteel.auth.config.AuthProps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * 过滤器
 */
@Component
@Slf4j
public class UsernameRequestFilter extends OncePerRequestFilter {

    @Resource
    private AuthProps authProps;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String jwt = request.getHeader(HttpHeaders.AUTHORIZATION);

        String url = request.getRequestURI();

        if (authProps.getExcludePaths().contains(url)) {
            chain.doFilter(request, response);
            return;
        }

        log.info(">>>" + jwt);


        chain.doFilter(request, response);
    }
}

OAuth2ServerConfig

这个类是启用认证服务器的核心配置,具体看代码:

package com.banksteel.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * @author gehao
 * @since 2020/8/26
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager authenticationManagerBean;

    @Resource
    private DataSource dataSource;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer()).authenticationManager(authenticationManagerBean);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer());
    }

    private JwtAccessTokenConverter jwtTokenEnhancer() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("{私钥文件名}.jks"),
                "{私钥密码}".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("{私钥别名}"));
        return converter;
    }
}

注意,这个配置文件看起来比较简单,但是涉及到的东西比较多,一个是之前准备好的公私钥,还有一个是之前初始化好的数据库。

ResourceServerConfig

加入这个配置,就启用了资源服务器。

package com.banksteel.auth.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

import javax.annotation.Resource;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Resource
    private AuthProps authProps;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("main-gateway") // 配置资源id,这里的资源id和授权服务器中的资源id一致,在数据库中要初始化好
                .stateless(true); // 设置这些资源仅基于令牌认证
    }

    /**
     * 配置 URL 访问权限
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("管理员")
                .antMatchers("/user/**").hasRole("user")
                .antMatchers(authProps.getExcludePaths().toArray(new String[0])).permitAll()
                .anyRequest().authenticated();
    }
}

HelloController

主要用来测试

package com.banksteel.auth.controller;

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

@RestController
public class HelloController {

    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }

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

开始测试

在Postman中设置(如果图片看不清可以右键在新标签中打开图片查看)

点击 Get New Access Token:

蓝色选择 Password Credentials。

绿色是 用户名密码

红色是 客户端配置

点击 Request Token 按钮,得到 JWT:

然后点击 Use Token 按钮,发起请求:

可以将隐藏的 Header 打开,可以看到在Authorization字段中设置了 Token。

Kehaw

👨‍💻Ke Haw 🇨🇳👨‍👩‍👧‍👦

风吹云散去,夜色好观星
Java | 前端 | 大数据

专注于 Spring Cloud 微服务架构与数据处理,研究一切与Java相关的开发技术,包括一部分前端技术。

目前的工作主要是关于B2B大宗商品在线交易领域的数据处理。如果对本站的部分内容感兴趣,请通过邮件、Twitter联系我🤝。

Fork me on Gitee
基于Spring Security + OAuth2 + JWT 的权限认证(一) Java-Stream学习第四步:数据处理 Java-Stream学习第三步:终端操作 Java-Stream学习第二步:处理流 Java-Stream学习第一步:创建流 Electron使用串口通信 Electron下调用DLL文件 国外SaaS服务供应商都是干什么的:Part1 为什么Kafka会丢失消息 Spring Boot中使用JSR380验证框架
Description lists
Kehaw's blog
Site description
人初做事,如鸡伏卵,不舍而生气渐充;如燕营巢,不息而结构渐牢;如滋培之木,不见其长,有时而大;如有本之泉,不舍昼夜,盈科而后进,放乎四海。
Copyright
© 2014 Copyright Kehaw | All rights reserved.