17. Oauth2-microservice authentication

1.Oauth2

The OAuth 2.0 authorization framework supports third-party support for limited access to HTTP services, through an approval interaction between the resource owner and the HTTP service to access these resources on behalf of the resource owner, or by allowing third-party applications to obtain access on their own behalf. authority.

image-20220506121118022

For the convenience of understanding, it can be imagined that OAuth2.0 is an intermediate layer between user resources and third-party applications. It separates resources from third-party applications, so that third-party applications cannot directly access resources, thereby protecting resources. .

In order to access such protected resources, third-party applications (clients) need to provide credentials when accessing. That is, you need to tell OAuth2.0 who you are and what you want to do.

The user can tell the third-party application the user name and password, so that the third-party application can directly access it in your name, or authorize the third-party application to access it.

For example, in the development of the WeChat public platform, when we visit a certain page during the development of the WeChat public platform, a prompt box may pop up on the page that the application needs to obtain our personal information and ask whether to allow it. Clicking confirm is actually authorizing a third-party application to obtain our WeChat account. For the personal information of the public platform, the authorization of the WeChat webpage here is to use OAuth2.0.

  • Third-party application: Also known as client, various clients developed by ourselves. For our own projects, QQ, WeChat, Alipay, etc. are third-party applications.

  • HTTP service provider (HTTP service): The projects we develop, as well as QQ, WeChat, Alipay, DingTalk, etc., can all be called "service providers".

  • Resource Owner: Also known as a user, a person who has an account password.

  • User Agent (User Agent): Used to access resources, such as browsers, to access these resources instead of users.

  • Authentication server (Authorization server): It is a server specially used by service providers to handle authentication, mainly to realize login and authorization functions.

  • Resource server (Resource server): The server where the service provider stores the resources generated by the user, such as the product module and order module in e-commerce, and is used to process specific businesses.

        The OAuth2.0 protocol flow describes the interaction process between the four roles, as shown in the figure below.

image-20220506121307806

        Simply put, OAuth is an authorization mechanism. The owner of the data tells the system that he agrees to authorize third-party applications to enter the system and obtain the data. The system thus generates a short-term access token (token), which is used instead of a password for use by third-party applications.

The role of token (token) and password (password) is the same, both can enter the system, but there are three differences.

  • The token is short-lived and will automatically expire when it expires, and the user cannot modify it by himself. The password is generally valid for a long time, and it will not change if the user does not modify it.

  • Tokens can be revoked by the data owner and expire immediately.

  • Tokens have a scope. For web services, read-only tokens are more secure than read-write tokens. The password is generally full permissions.

The above designs ensure that the token can not only allow third-party applications to obtain permissions, but also be controllable at any time without endangering system security. This is the beauty of OAuth 2.0.

Note that as long as you know the token, you can enter the system. The system generally does not confirm the identity again, so the token must be kept secret, and the consequences of leaking the token are the same as leaking the password. This is why the validity period of the token is generally set very short.

1.1 Open Platform

        Open Platform (Open Platform) In the software industry and the Internet, an open platform refers to a software system that exposes its application programming interface (API) or function (function) so that external programs can increase the functionality of the software system or use the software. resources of the system without requiring changes to the source code of the software system.

        In the Internet age, the behavior of encapsulating website services into a series of easily recognizable data interfaces for third-party developers is called Open API, and the platform that provides open APIs is called an open platform.

        The first is technical openness, such as Baidu, Tencent, Alibaba, etc. For example, Ali can provide standardized application software, but millions of sellers of all kinds have personalized software that cannot be satisfied by a single company. Therefore, these requirements are opened to many third-party developers. Another example is Google's open-source mobile phone operating system based on the Linux platform, which is believed to soon defeat Nokia's Symbian system. Although this kind of technical open platform has little to do with the open platforms of B2C companies at present, it can also explain to a certain extent that open platforms are the trend of Internet companies.

        The second type of open platform refers to a software system that exposes its application programming interface (API) or function (function), so that external programs can increase the functions of the software system or use the resources of the software system without changing the software. System source code. The B2C enterprise open platform also includes two forms, A: Taobao Mall, Japan’s Rakuten, a pure platform model, that is, the purchase, sale and storage of products without touching them, all of which are done by the settled merchants; B: American Amazon, Dangdang, JD.com The "self-operation + joint operation" model of the mall.

1.2 Open Platform Interaction Model

Three roles:

  • Resource Owner: User

  • Client: various apps, browsers

  • Service Provider: Contains two roles

    authentication server

    resource server

1.2.1 Authentication server

The authentication server is responsible for authenticating users and authorizing permissions to clients. General authentication is achieved by verifying the account password, but the difficulty lies in how to authorize. For example, if we use a third-party to log in to "Bilibili", we can see the words "Bilibili will obtain the following permissions" and permission information on the authorization page of QQ login

image-20220506121510936

image-20220506121520612

The authentication server needs to know the identity of the client requesting authorization and the permissions requested by the client. A common practice is to pre-allocate an id for each client, and assign a name and permission information to each id. This information can be written in the configuration file on the authentication server. Every time the client opens the authorization page in the future, the client needs to send the id to the authentication server. 0Auth2.0 can be used to automatically assign the id to the client. At the same time Complete automatic update of configuration files.

1.3 OAuth2 Open Platform

The open platform is a product developed from the OAuth2.0 protocol. Its function is to allow the client to register and apply on it. After passing the system, the system automatically assigns the client ID and completes the automatic update of the configuration.

In order for the client to complete the application, the applicant usually needs to fill in the type of client program (Web, App, WeChat applet, Alipay applet, etc.), company information, business license, legal person information, and the information you want to obtain permissions. The development platform will automatically assign a client id to the client after getting the approval of the service provider.

After passing the audit, the third-party application will display the permission information that needs to be obtained on the page when performing authentication, for example, Bilibili obtains QQ permission. After the authorization is successful, the authentication server needs to send the generated access_token to the client, so that the client can access specific resources (such as avatar, gender), and the general process is as follows:

  • Let the client fill in a URL when submitting an application on the open platform, for example: www.baidu.com, this URL is mainly used to obtain the authentication code.

  • When a user is authorized successfully, the authentication server will redirect the page to this URL, and splice the generated access_token to the URL, for example: www.baidu.com?access_token=123 

  • The client receives the access_token, and then the client can use this token to obtain the required data

1.3.1 Tokens

Traditional projects request data from the server, and the server needs to frequently go to the database to query the user name and password and compare them to determine whether the user name and password are correct or not, and give corresponding prompts. This is very inefficient. How to improve efficiency? Token came into being.

Token is a string of strings generated by the server as a token for the client to request. After logging in for the first time, the server generates a Token and returns the Token to the client. In the future, the client only needs to bring this Token can come to request data, no need to bring user name and password again. Reduce the pressure on the server, reduce frequent query database, and make the server more robust.

1.3.2 Access Token

Access Token is the token for the client to access the resource server. Possessing this token represents the authorization of the user, that is, the right to access resources. At the same time, this authorization should be temporary and can only be used within a certain period of time. The main reason is that the Access Token is likely to be leaked during use and used by criminals to obtain our data. Therefore, Access Token should only be used within a certain period of time, which can reduce the risk caused by the leakage of Access Token.

1.4 Authentication Mode

Four authorization modes are defined in OAuth2.0:

  • authorization code authorization code mode

  • implicit simplified mode

  • resource owner password credentials password mode

  • client credentials client mode

Common modes: authorization code, password mode

1.4.1 Authorization code mode

The authorization code mode (authorization code) is the authorization mode with the most complete functions and the most rigorous process. The code guarantees the security of the token. Even if the code is intercepted, the token cannot be obtained through the code because there is no secret.

Character Behavior and Function
  • resource owner

    Just allow or deny authorization for third-party apps

  • third-party usage

    Apply for a third-party application to be a resource server

    Get the resources provided by the resource server

  • authorization server

    Provide authorization license code, token token, etc.

  • resource server

    Provide open resource interfaces for third-party applications

image-20230423093957585

Timing diagram

image-20230423094544416

Environment build

create parent project

image-20220506141039544

image-20220506141533483

Specify the packaging method as 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.woniuxy</groupId>
    <artifactId>oauth2</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
</project>

Create auth-server authentication server module

image-20220506141646384

image-20220506141740947

import dependencies

image-20220506141849136

The import dependency version is as follows

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
</properties>

oauth2 dependency

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.2.4.RELEASE</version>
</dependency>

Create user information configuration class

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter{
	
	//密码编码器
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
    // 基于内存的用户信息
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication()	//内存认证
			.withUser("zhangsan")		//用户名
			.password(passwordEncoder().encode("123"))	//密码
			.authorities("ROLE_ADMIN");	//角色
	}
}

Create a client configuration class and configure client information

import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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 javax.annotation.Resource;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{
	
	@Resource
	private BCryptPasswordEncoder passwordEncoder;
	
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		//配置客户端
		clients
			.inMemory()		//内存方式
			.withClient("client")	//客户端名字
			.secret(passwordEncoder.encode("secret"))	//客户端秘钥
			.authorizedGrantTypes("authorization_code")//授权类型
			.scopes("all")	//授权范围
			.redirectUris("http://www.baidu.com");	//回调网址,携带授权码
	}
}

Configure the following information in the application.yml file

server:
  port: 8000
spring:
  application:
    name: oauth

Start the project to log in

localhost:8000/login

Enter the login page, enter account number: zhangsan, password: 123 to log in

image-20220506143057117

After successful login, send a request to the server to obtain the authorization code, enter the following content on the address bar and press Enter

http://localhost:8080/oauth/authorize?client_id=client&response_type=code

You can see an authorization page, asking the user whether to authorize

image-20220506143330317

After successful authorization, it will redirect to the address specified in the AuthorizationServerConfiguration configuration class, and carry the authorization code as a parameter

Send a request to the server to get a token through postman

Fill in the address bar: http://client:secret@localhost:8000/oauth/token

Fill in the client account password

image-20230831110955586

Fill in the authorization type, authorization code, and send the request

image-20220506145021696

After success, you can see the following information on postman

image-20220506145131136

express success

Note: Each authorization code can only be used once

1.4.2 Password mode

In password mode (Resource Owner Password Credentials Grant), the user provides his user name and password to the client. The client uses this information to request authorization from the "service provider".

In this mode, the user must give his password to the client, but the client must not store the password. This is usually used when the user has a high degree of trust in the client, such as the client being part of the operating system, or produced by a well-known company. The authentication server can only consider using this mode if other authorization modes cannot be implemented.

Modify the AuthorizationServerConfiguration configuration class and add a password mode

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //配置客户端
    clients
        .inMemory()		
        .withClient("client")
        .secret(passwordEncoder.encode("secret"))
        .authorizedGrantTypes("authorization_code","password") //添加密码授权模式
        .scopes("all")	//授权范围
        .redirectUris("http://www.woniuxy.com");
}

Open a new request in postman, fill in the address bar: http://localhost:8080/oauth/token

The password authorization mode requires the client account password to be submitted as a request header, and base64 encryption is required for the account password, so select the Authorization tab, set TYPE to "Basic Auth", and fill in the client account password

image-20220506151829604

Set the authorization type, user account and password parameters in the request body

image-20220506151940710

Send request test

image-20220506152019225

It can be found that the password mode is not supported at this time, even if the password mode is specified in the AuthorizationServerConfiguration configuration class.

The reason is that the code lacks support for the password mode at this time, and an AuthenticationManager object needs to be added in oauth2 to support the password mode.

Configure the AuthenticationManager in the WebSecurityConfiguration configuration class

// 配置 AuthenticationManager(密码模式需要该对象进行账号密码校验)
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

Inject AuthenticationManager in the AuthorizationServerConfiguration class and override the following methods

// 认证管理器
@Autowired
private AuthenticationManager authenticationManager;

//配置使用的 AuthenticationManager 实现用户认证的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.authenticationManager(authenticationManager);
}

Restart the project and send the request again to get the token

image-20220506152551071

Integrate JWT

JWT-related dependencies are automatically imported after importing oauth2 dependencies, so there is no need to import JWT separately, just need to set it up

Create a TokenConfiguration configuration class

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
public class TokenConfiguration {
    // 密码
    private static String SIGNING_KEY="www.woniuxy.com";

    // token转换器
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = 
            new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }

    // 令牌存储策略:jwt方式
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(accessTokenConverter());
    }
}

Inject related objects in the AuthorizationServerConfiguration configuration class

@Resource
private TokenStore tokenStore;

@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Resource
private ClientDetailsService clientDetailsService;

Write the token service method in the AuthorizationServerConfiguration configuration class, which is mainly used to set

private AuthorizationServerTokenServices tokenServices(){
    // 创建服务对象
    DefaultTokenServices services = new DefaultTokenServices();
    // 设置客户端详情服务
    services.setClientDetailsService(clientDetailsService);
    // 支持刷新令牌
    services.setSupportRefreshToken(true);
    // 不重复使用refreshtoken,每次刷新之后只能用新的refreshtoken才能继续刷新
	services.setReuseRefreshToken(false);
    // 设置令牌存储策略
    services.setTokenStore(tokenStore);

    // 设置令牌增强
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
    services.setTokenEnhancer(tokenEnhancerChain);

    // 设置令牌过期时间
    services.setAccessTokenValiditySeconds(600);
    services.setRefreshTokenValiditySeconds(6000);

    return services;
}

Modify the configure(AuthorizationServerEndpointsConfigurer endpoints) method to add token service

//配置使用的 AuthenticationManager 实现用户认证的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
        .authenticationManager(authenticationManager) // 认证管理器
        .tokenServices(tokenServices());	// 配置token服务
}

Restart the project and send a request to get the token

image-20220506160230848

If you want to obtain refreshtoken, you can modify the AuthorizationServerConfiguration configuration class and add the refresh_token authorization method

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //配置客户端
    clients
        .inMemory()		
        .withClient("client")
        .secret(passwordEncoder.encode("secret"))	
        .authorizedGrantTypes("authorization_code","password","refresh_token") 
        .scopes("all")
        .redirectUris("http://www.woniuxy.com");
}

Restart project test

image-20220506161053399

image-20220506161135294

Integrated database (user)

Create table SQL

create database sc default character set=utf8;

DROP TABLE IF EXISTS `perms`;
CREATE TABLE `perms` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `perms` VALUES (3001,'user:add'),(3002,'user:del'),(3003,'user:find'),(3004,'user:update'),(3005,'goods:add'),(3006,'goods:find'),(3007,'goods:del'),(3008,'goods:update');

DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `role` VALUES (2001,'ROLE_ADMIN'),(2002,'ROLE_USER');


DROP TABLE IF EXISTS `role_perms`;
CREATE TABLE `role_perms` (
  `rid` int(11) DEFAULT NULL,
  `pid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `role_perms` VALUES (2001,3001),(2001,3003),(2001,3004),(2002,3005),(2002,3006),(2002,3007),(2002,3008);

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) DEFAULT NULL,
  `username` varchar(20) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES (1001,'zhangsan','$2a$10$pINVnd8.cXScFXCxI2x4cem4fOexA2J5TNY/Mx2CjN6mJuYGBNG0m'),(1002,'wangwu','wangwu');

DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user_role` VALUES (1001,2001),(1002,2002),(1003,2002);

Introduce mybatis in pom.xml of auth-server

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>

Configure mybatis parameters in application.yml

mybatis:
  type-aliases-package: com.woniuxy.authserver.entity
  mapper-locations: classpath:/mapper/*.xml

Create Perms, Role, and User entity classes. Note: the entity class must implement the serialization interface, otherwise a Failed to find access token for token error may be reported during operation.

import lombok.Data;

@Data
public class Perms implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
}
import lombok.Data;
import java.util.List;

@Data
public class Role implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    private List<Perms> perms;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails, Serializable {

    private static final long serialVersionUID = 1L;

    private int id;
    private String username;
    private String password;
    private List<Role> roles;

    // 返回当前用户的所有角色、权限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        log.debug("获取用户角色权限信息");
        // 新建集合
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        // 遍历role
        for(Role role : this.roles){
            // 放入角色信息
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
            // 遍历当前角色的所有权限信息
            for(Perms perms : role.getPerms()){
                grantedAuthorities.add(new SimpleGrantedAuthority(perms.getName()));
            }
        }
        log.debug(grantedAuthorities.toString());
        return grantedAuthorities;
    }

    // 获取用户名
    @Override
    public String getUsername() {
        return this.username;
    }

    // 账号是否过期    true表示未过期   false表示过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 账号是否被锁定  true表示未锁定   false表示锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 凭证是否过期  true表示未过期   false表示过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 用户是否被禁用  true表示未禁用   false表示禁用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

Create UerMapper interface

import com.woniuxy.springsecurity.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    public User findByName(String username);
}

Create a mapper folder in the resources directory, and create a Mapper file under this folder

image-20220416173702294

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.woniuxy.authserver.mapper.UserMapper" >
    <select id="findByName" resultMap="user_map">
        select * from user where username = #{username}
    </select>

    <resultMap id="user_map" type="User">
        <id column="id" property="id"></id>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>

        <collection property="roles" ofType="Role" column="id" select="findRolesByUid"></collection>
    </resultMap>

    <select id="findRolesByUid" resultMap="role_map">
        select r.id,r.name from user_role ur,role r where ur.rid = r.id and ur.uid = #{id}
    </select>
    <resultMap id="role_map" type="Role">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>

        <collection property="perms" ofType="Perms" column="id" select="findPermsByRid"></collection>
    </resultMap>

    <select id="findPermsByRid" resultType="Perms">
        select p.id,p.name from role_perms rp,perms p where rp.pid = p.id and rp.rid = #{rid}
    </select>
</mapper>

Create a CustomUserDetailsServiceImpl class to implement the UserDetailsService interface

import com.woniuxy.authserver.entity.User;
import com.woniuxy.authserver.mapper.UserMapper;
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;

@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.查询用户
        User user = userMapper.findByName(username);

        //2.判断
        if (user == null) throw new UsernameNotFoundException("用户不存在");

        //3.返回用户信息
        return user;
    }
}

Inject the UserDetailsService object in the configuration class WebSecurityConfiguration, and modify configure (AuthenticationManagerBuilder auth) to obtain the specified user information from the database

@Resource
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //auth
        //.inMemoryAuthentication()	//内存认证
        //.withUser("zhangsan")		//用户名
        //.password(passwordEncoder().encode("123"))	//密码
        //.authorities("ROLE_ADMIN");	//角色
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

Restart the auth-server service for authentication

Package user id

When generating the token, the user id can be encapsulated into the token for later use

Modify the accessTokenConverter() method in the TokenConfiguration class, and rewrite the enhance method when creating the converter

// token转换器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = 
        new JwtAccessTokenConverter(){
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            final Map<String,Object> map = new HashMap<>();
            // 从认证对象中得到用户信息
            User user = (User) authentication.getUserAuthentication().getPrincipal();
            // 将用户id放到token中
            map.put("uid", user.getId());
            ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(map);
            // 返回
            return super.enhance(accessToken, authentication);
        }
    };
    jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
    return jwtAccessTokenConverter;
}

Test with postman

image-20220726160937503

You can see the user id in the returned result, and the token also contains the user id

Check whether the token has expired

The org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint class defines the token verification interface /oauth/check_token, which can be used to verify whether the token is legal, expired, or forged

@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {

    OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    if (token == null) {
        throw new InvalidTokenException("Token was not recognised");
    }

    if (token.isExpired()) {
        throw new InvalidTokenException("Token has expired");
    }

    OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

    Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);

    // gh-1070
    response.put("active", true);	// Always true if token exists and not expired

    return response;
}

It’s just that the interface oauth2 is not open to the public by default. If you want to use this interface, you must manually configure and enable it. Rewrite the following methods in the AuthorizationServerConfiguration configuration class

//设置 /oauth/check_token 端点,通过认证后可访问。
//该端点对应 CheckTokenEndpoint类,用于校验访问令牌的有效性。
//在客户端访问资源服务器时,会在请求中带上访问令牌。
//在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    // 默认是denyAll():拒绝所有
    oauthServer.checkTokenAccess("permitAll()");
}

There are three common values ​​of checkTokenAccess:

  • denyAll(): reject all requests, do not open this interface

  • isAuthenticated(): Only open to requests after authentication

  • permitAll(): open to all requests

Test: After successful login, send a request in Postman for testing

Interface url: http://localhost:8080/oauth/check_token

image-20220726114103407

The returned result contains information such as the user's username and permissions, and also includes information about whether the token is available

If the following information is returned, the token has expired

image-20230831142542647

And if the following information is returned, the token is illegal

image-20230831142606770

Obtain a new token through refresh_token

The same interface is used to obtain the token and refresh the token, so the url in the address bar is still

http://local:8080/oauth/token

It's just that grant_type needs to be replaced with refresh_token, and then the previous refresh token is passed to the background as a parameter

image-20220507100748397

It is still necessary to put the client id and password in base64 encoding into the request header

image-20220507100833359

Send a request to get the result

image-20220507101406277

According to the results, it can be known that both token and refresh_token will be automatically refreshed. The advantage of this is that when the token expires, the refresh interface is called through the program to obtain a new token and refresh_token to realize automatic renewal.

If refresh_token expires, the following results will be obtained

image-20220726115252787

You need to log in again when the refresh_token expires

1.5 Resource Server

Create a resource submodule and import related dependencies

image-20220507112247458

Set parent-child relationship

Create OAuth2ResourceServerConfig configuration class

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;

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            // 设置请求,需要认证后访问
            .anyRequest().authenticated();
    }
}

create controller

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

@RestController
@RequestMapping("/resource")
public class ResourceController {

    @RequestMapping("/info")
    public String info(){

        return "success";
    }
}

Configure application.yml

server:
  port: 8001
spring:
  application:
    name: resource
security:
  oauth2:
    # OAuth2 Client 配置,对应 OAuth2ClientProperties 类
    client:
      client-id: client
      client-secret: secret
    # OAuth2 Resource 配置,对应 ResourceServerProperties 类
    resource:
      token-info-uri: http://127.0.0.1:8000/oauth/check_token # 获得 Token 信息的 URL
    # 访问令牌获取 URL,自定义的
    access-token-uri: http://127.0.0.1:8000/oauth/token
management:
  endpoints:
    web:
      exposure:
        include: '*'

Start the resource resource server

Authenticate first, get token and refresh_token

localhost:8000/oauth/token

image-20220507115312984

Then put the obtained token into the request header of the request resource server

image-20220507115927410

After sending the request, a 500 error can be found, and the following information can be found by checking the resource console

org.springframework.web.client.HttpClientErrorException$Forbidden: 403 : [{"timestamp":"2022-05-07T03:40:14.063+00:00","status":403,"error":"Forbidden","message":"","path":"/oauth/check_token"}]

According to the information prompt: No permission to access /oauth/check_token, this URL is the interface used by the authentication server to verify whether the token is legal. The resource server will obtain the token when receiving the request, and then call the /oauth/check_token interface of the authentication server to check the token. However, the authentication server has not opened the port at this time (closed by default), so 403 cannot be accessed.

Open /oauth/check_token in the AuthorizationServerConfiguration configuration class of the authentication server

//设置 /oauth/check_token 端点,通过认证后可访问。
//该端点对应 CheckTokenEndpoint类,用于校验访问令牌的有效性。
//在客户端访问资源服务器时,会在请求中带上访问令牌。
//在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    // 默认是denyAll():拒绝所有
    oauthServer.checkTokenAccess("isAuthenticated()");
}

Restart the authentication server

Re-authenticate to get the token, and then use the new token to access the resource server again

image-20220507120732391

Seeing success indicates success

Role permission management

Add the @EnableGlobalMethodSecurity annotation to the main startup class of the resource server to enable the support of spring security permission annotations

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

Add annotation @PreAuthorize on the resource/info interface method and specify roles or permissions

@RequestMapping("/info")
@PreAuthorize("hasRole('USER')")
public String info(){

    return "success";
}

Use postman to access the interface again

image-20220507150406869

The result of not allowing access is obtained, indicating that the role permission management is in effect

1.6 Integrated database (client)

Create table SQL

CREATE TABLE `clientdetails` (
  `appId` VARCHAR(128) NOT NULL,
  `resourceIds` VARCHAR(256) DEFAULT NULL,
  `appSecret` VARCHAR(256) DEFAULT NULL,
  `scope` VARCHAR(256) DEFAULT NULL,
  `grantTypes` VARCHAR(256) DEFAULT NULL,
  `redirectUrl` VARCHAR(256) DEFAULT NULL,
  `authorities` VARCHAR(256) DEFAULT NULL,
  `access_token_validity` INT(11) DEFAULT NULL,
  `refresh_token_validity` INT(11) DEFAULT NULL,
  `additionalInformation` VARCHAR(4096) DEFAULT NULL,
  `autoApproveScopes` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`appId`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_access_token` (
  `token_id` VARCHAR(256) DEFAULT NULL,
  `token` BLOB,
  `authentication_id` VARCHAR(128) NOT NULL,
  `user_name` VARCHAR(256) DEFAULT NULL,
  `client_id` VARCHAR(256) DEFAULT NULL,
  `authentication` BLOB,
  `refresh_token` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_approvals` (
  `userId` VARCHAR(256) DEFAULT NULL,
  `clientId` VARCHAR(256) DEFAULT NULL,
  `scope` VARCHAR(256) DEFAULT NULL,
  `status` VARCHAR(10) DEFAULT NULL,
  `expiresAt` TIMESTAMP NULL DEFAULT NULL,
  `lastModifiedAt` TIMESTAMP NULL DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_client_details` (
  `client_id` VARCHAR(128) NOT NULL,
  `resource_ids` VARCHAR(256) DEFAULT NULL,
  `client_secret` VARCHAR(256) DEFAULT NULL,
  `scope` VARCHAR(256) DEFAULT NULL,
  `authorized_grant_types` VARCHAR(256) DEFAULT NULL,
  `web_server_redirect_uri` VARCHAR(256) DEFAULT NULL,
  `authorities` VARCHAR(256) DEFAULT NULL,
  `access_token_validity` INT(11) DEFAULT NULL,
  `refresh_token_validity` INT(11) DEFAULT NULL,
  `additional_information` VARCHAR(4096) DEFAULT NULL,
  `autoapprove` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_client_token` (
  `token_id` VARCHAR(256) DEFAULT NULL,
  `token` BLOB,
  `authentication_id` VARCHAR(128) NOT NULL,
  `user_name` VARCHAR(256) DEFAULT NULL,
  `client_id` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_code` (
  `code` VARCHAR(256) DEFAULT NULL,
  `authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_refresh_token` (
  `token_id` VARCHAR(256) DEFAULT NULL,
  `token` BLOB,
  `authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;

Add a client configuration record in the table oauth_client_details, which can be configured according to the client configuration in the AuthorizationServerConfiguration configuration class when filling in

The effect of the configuration is as follows:

image-20220507161710501

Note: Explanation of each field

  • client_id: client ID

  • client_secret: client security code. Note that the security code cannot be in plain text and needs to be encrypted. You can write a program here, and then use BCryptPasswordEncoder to encrypt the client security code, get the encrypted security code, and then write it into the database, for example:

  • System.out.println(new BCryptPasswordEncoder().encode("secret"));

  • scope: client authorization scope

  • authorized_grant_types: client authorization type, supports multiple types, separated by commas

  • web_server_redirect_uri: server callback address

Create entity classes User, Role, Perms

Introduce mybatis related dependencies in the pom.xml of the auth-server module

<!-- spring-boot-starter-jdbc 内置了HikariCP 连接池,所以使用该连接池连接数据库 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 获取application.yml文件中的配置 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

Add database-related configuration to the application.yml file

server:
  port: 8000
spring:
  application:
    name: oauth
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://localhost:3306/sc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root
    hikari:
      minimum-idle: 5
      maximum-pool-size: 10
      auto-commit: true #自动提交
      pool-name: MYHIKARICP
      connection-test-query: SELECT 1 #测试是否能连接上数据库的SQL语句
  main:
    #true,后定义的bean会覆盖之前定义的相同名称的bean,生成dataSource替换掉原生的dataSource
    allow-bean-definition-overriding: true

Create the database configuration class DataSourceConfiguration, which mainly configures the data source used, and replaces the data source of the HikariCP connection pool with the built-in data source of spring.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfiguration {
    @Bean   
    @Primary    
    //根据application.yml中的配置信息创建dataSource
    @ConfigurationProperties(prefix = "spring.datasource")
    //import javax.sql.DataSource;
    public DataSource dataSource() {
        //创建dataSource
        return DataSourceBuilder.create().build();
    }
}

Change the token storage strategy to JDBC in the TokenConfiguration configuration class, and store jwt in the database DataSource

@Resource
private DataSource dataSource;

// 令牌存储策略:jwt方式
@Bean
public TokenStore tokenStore(DataSource dataSource){
    //return new JwtTokenStore(accessTokenConverter());
    return new JdbcTokenStore(dataSource);
}

Modify the AuthorizationServerConfiguration configuration class, add the ClientDetailsService clientDetailsService(DataSource dataSource) method, let the program obtain the client information from the database through the DataSource

@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
    //在数据库中去获取客户端信息(oauth_client_details表)
    return new JdbcClientDetailsService(dataSource);
}

Modify the configure(ClientDetailsServiceConfigurer clients) method to specify the database to obtain client information

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //配置客户端
    //clients
    //.inMemory()		//内存方式
    //.withClient("client")	//客户端名字
    //.secret(passwordEncoder.encode("secret"))	//客户端秘钥
    //.authorizedGrantTypes("authorization_code","password","refresh_token")
    //.scopes("all")	//授权范围
    //.redirectUris("http://www.woniuxy.com");	//回调网址

    clients.withClientDetails(clientDetailsService);
}

Restart the project after completion and perform the certification test again

Under normal circumstances, after the test is completed, a record will be added to the oauth_access_token table of the database. This record is the token and refresh token obtained by the browser

image-20220509101431817

Role, permission management test

Add the following method to the controller of the resource service

@RequestMapping("/message")
@PreAuthorize("hasRole('ADMIN')")
public String message(){

    return "message";
}

@RequestMapping("/data")
@PreAuthorize("hasAuthority('user:add')")
public String data(){

    return "data";
}
@RequestMapping("/test")
@PreAuthorize("hasAuthority('user:del')")
public String test(){

    return "test";
}

Start the resource service, and test the info, message, data, and test interfaces through postman. If only the message and data interfaces can be accessed, it means that the role and authority management is successful.

Guess you like

Origin blog.csdn.net/LB_bei/article/details/132607336