Set up an OAUTH2 server, including authentication, authorization, and resource servers

References:

www.cnblogs.com/fp2952/p/89…

Juejin. Cn/post / 684490…

Spring OAuth2 official documentation

This paper is divided into two parts

  • The first part is relatively simple. The client and user information is fixed in the program, and the token is stored in memory
  • The second part reads the user information from the database and generates the token using JWT

Project address: github.com/zheyday/Spr…

Request branch

A simplified version

To create a new project using Spring Initializr, check the following three options

pom.xml

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>// Spring-security-oauth2 spring-security-jwt spring-security-oauth2-autoconfigure<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
Copy the code

Configure Spring Security

New class WebSecurityConfig WebSecurityConfigurerAdapter inheritance, and add @ Configuration @ EnableWebSecurity annotations, rewrite the three methods, the code is as follows, in detail under the code

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceDetail userServiceDetail;

    @Bean
    public PasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
        // Memory storage
// auth
// .inMemoryAuthentication()
// .passwordEncoder(passwordEncoder())
// .withUser("user")
// .password(passwordEncoder().encode("user"))
// .roles("USER");

    }


    /** * Configure the default form login and disable CSRF function, and enable httpBasic authentication **@param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http    // Configure the login page /login and allow access
                .formLogin().permitAll()
                / / logout page
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // All other requests require authentication
                .and().authorizeRequests().anyRequest().authenticated()
                // Since we are using JWT, we do not need CSRF here
                .and().csrf().disable();
    }

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

I’m going to focus on that

protected void configure(AuthenticationManagerBuilder auth) throws Exception
Copy the code

This method is used to validate user information. Match the user name and password entered in front with the database. If there is a user, the authentication can be successful. We inject a UserServiceDetail, and the service’s function is authentication. . PasswordEncoder (passwordEncoder()) is used for salt decryption.

UserServiceDetail

The UserDetailsService interface is implemented, so you need to implement a unique method

package zcs.oauthserver.service;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 zcs.oauthserver.model.UserModel;

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

@Service
public class UserServiceDetail implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE"));
        return new UserModel("user"."user",authorities); }}Copy the code

Here we implement the function with dummy parameters first, and then add the database

Parameter S is the user name entered by the front end, through which the database is searched and the password and role permissions are obtained. Finally, these three data are encapsulated into the implementation class of the UserDetails interface and returned. Here encapsulated class can use org. Springframework. Security. Core. Populated userdetails. User or populated userdetails interface.

UserModel

Implement the UserDetails interface

package zcs.oauthserver.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

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

public class UserModel implements UserDetails {
    private String userName;

    private String password;

    private List<SimpleGrantedAuthority> authorities;

    public UserModel(String userName, String password, List<SimpleGrantedAuthority> authorities) {
        this.userName = userName;
        this.password = new BCryptPasswordEncoder().encode(password);;
        this.authorities = authorities;
    }

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

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

    @Override
    public String getUsername(a) {
        return userName;
    }

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

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

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

    @Override
    public boolean isEnabled(a) {
        return true; }}Copy the code

Add username, Password, and authorities. The last one stores a list of the user’s permissions, that is, what resources the user has access to. Password salt processing.

Configure the Oauth2 authentication server

The new configuration class AuthorizationServerConfigurerAdapter AuthorizationServerConfig inheritance, And add @ Configuration @ EnableAuthorizationServer annotation suggests that is an authentication server

Rewrite three functions

  • ClientDetailsServiceConfigurer: used to configure the customer call service, the customer call information is initialized here, you can write the customer call information here or store the details from the database. A client is a third-party application
  • AuthorizationServerSecurityConfigurer: configures security constraints for Token endpoints.
  • AuthorizationServerEndpointsConfigurer: Used to configure authorization and token access endpoints and token services.
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
	// load from WebSecurityConfig
    @Autowired
    private AuthenticationManager authenticationManager;
    // Memory stores tokens
    private TokenStore tokenStore = new InMemoryTokenStore();

    /** * Configure client details **@param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            	// Client ID
                .withClient("zcs")
                .secret(new BCryptPasswordEncoder().encode("zcs"))
                // Scope of permission
                .scopes("app")
            	// Authorization code mode
                .authorizedGrantTypes("authorization_code")
                / / write casually
                .redirectUris("www.baidu.com");
// clients.withClientDetails(new JdbcClientDetailsService(dataSource));
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager);
    }

    /** * Define security constraints on token endpoints * to allow form validation, the browser simply sends a POST request to get tocken *@param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // Enable /oauth/token_key authentication port without permission access
                .tokenKeyAccess("permitAll()")
                // Enable /oauth/check_token authentication port authentication permission access
                .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients(); }}Copy the code

The client details are also used for testing, and the database will be added later. The token service is stored in memory for the time being, followed by JWT.

Function first is the most important, complex things step by step to add.

Configuring a Resource Server

The resource server is the server that needs to be accessed

New ResourceServerConfig ResourceServerConfigurerAdapter inheritance

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
// antMatcher indicates that only requests from /user can be processed
                .antMatcher("/user/**")
                .authorizeRequests()
                .antMatchers("/user/test1").permitAll()
                .antMatchers("/user/test2").authenticated()
// .antMatchers("user/test2").hasRole("USER")
// .anyRequest().authenticated(); }}Copy the code

ResourceServerConfigurerAdapter Order the default value is 3, less than WebSecurityConfigurerAdapter, the smaller the value of the greater priority

About ResourceServerConfigurerAdapter and WebSecurityConfigurerAdapter see detailed instructions

www.jianshu.com/p/fe1194ca8…

New UserController

@RestController
public class UserController {
    @GetMapping("/user/me")
    public Principal user(Principal principal) {
        return principal;
    }

    @GetMapping("/user/test1")
    public String test(a) {
        return "test1";
    }

    @GetMapping("/user/test2")
    public String test2(a) {
        return "test2"; }}Copy the code

test

  1. Get code browser accesshttp://127.0.0.1:9120/oauth/authorize?client_id=zcs&response_type=code&redirect_uri=www.baidu.com“And then pop out of the login page,

The address bar will appear callback page, with http://127.0.0.1:9120/oauth/www.baidu.com?code=FGQ1jg code parameter

  1. Obtain token postman accesshttp://127.0.0.1:9120/oauth/token?code=FGQ1jg&grant_type=authorization_code&redirect_uri=www.baidu.com&client_id=zcs&cli ent_secret=zcs, code fills in the code you just got, using the POST request
  2. Access resource /user/test2 is a protected resource that we access through a token

Two, upgraded version

JWT

A lot of people compare JWT to OAuth2, but it’s completely different and not comparable.

JWT is an authentication protocol that provides a method for issuing access tokens and validating issued signed access tokens.

OAuth2 is an authorization framework that provides a detailed set of authorization mechanisms.

Spring Cloud OAuth2 integrates JWT as token management, making it easy to use

JwtAccessTokenConverter is a converter used to generate tokens, which by default are signed and the resource server needs to validate this signature. There are two encryption and verification modes: symmetric encryption and asymmetric encryption (public key) Symmetric encryption requires that the authorization server and the resource server store the same key value. Asymmetric encryption requires that the authorization server and the resource server store the same key value, and the public key is exposed to the resource server for verification. This section uses the asymmetric encryption mode.

Use the JDK tool to generate a JKS certificate, enter the bin directory of the JDK installation directory, and run the command

keytool -genkeypair -alias oauth2-keyalg RSA -keypass mypass -keystore oauth2.jks -storepass mypass

The oauth2. JKS file is generated in the current directory and placed in the resource directory.

Maven does not load files in the resource directory by default, so you need to configure them in pom.xml and add them under Build

	  <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>/ *. * * *</include>
                </includes>
            </resource>
        </resources>
    </build>

Copy the code

Changes in the original AuthorizationServerConfig code

	@Autowired
    private TokenStore tokenStore;	

	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// endpoints.tokenStore(tokenStore)
// .authenticationManager(authenticationManager);
        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenStore(tokenStore);
    }

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

    /** * Asymmetric encryption algorithm to sign the token *@return* /
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(a) {
        final JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
        // Import the certificate
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "mypass".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
        return converter;
    }

Copy the code

JwtAccessTokenConverter method have a CustomJwtAccessTokenConverter class, this is inherited jwtAccessTokenConverter, custom added additional token information

/** * Add additional token information */
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
        Map<String, Object> additionalInfo = new HashMap<>();
        UserModel user = (UserModel)authentication.getPrincipal();
        additionalInfo.put("USER",user);
        defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
        return super.enhance(defaultOAuth2AccessToken,authentication); }}Copy the code

Security

Login was done with fake data, now verify by connecting to the database.

Create three tables: user to store user accounts and passwords, role to store roles, and user_Role to store user roles

The user table

Role table

User_role table

Generate code using Mybatis -Plus to transform the previous UserServiceDetail and UserModel

UserServiceDetail

@Service
public class UserServiceDetail implements UserDetailsService {
    private final UserMapper userMapper;
    private final RoleMapper roleMapper;

    @Autowired
    public UserServiceDetail(UserMapper userMapper, RoleMapper roleMapper) {
        this.userMapper = userMapper;
        this.roleMapper = roleMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username", s);
        User user = userMapper.selectOne(userQueryWrapper);
        if (user == null) {
            throw new RuntimeException("Wrong username or password");
        }

        user.setAuthorities(roleMapper.selectByUserId(user.getId()));
        returnuser; }}Copy the code

The User information is queried by UserMapper, and then encapsulated in User. The UserDetails interface is implemented on the automatically generated User

User

public class User implements Serializable.UserDetails {

    private static final long serialVersionUID = 1L;

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

    @TableId(value = "username")
    private String username;

    @TableId(value = "password")
    private String password;

    @TableField(exist = false)
    private List<Role> authorities;

    public User(a) {}public Integer getId(a) {
        return id;
    }

    public String getUsername(a) {
        return username;
    }

    public String getPassword(a) {
        return password;
    }

    public void setPassword(String password) {
        this.password = new BCryptPasswordEncoder().encode(password);
    }

    public void setAuthorities(List<Role> authorities) {
        this.authorities = authorities;
    }

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

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

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

    @Override
    public boolean isEnabled(a) {
        return true;
    }

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


    @Override
    public String toString(a) {
        return "User{" +
                "id=" + id +
                ", username=" + username +
                ", password=" + password +
                "}"; }}Copy the code

Explanation:

You need to override a method in UserDetails that stores user permissions

@Override
public Collection<? extends GrantedAuthority> getAuthorities()
Copy the code

So a variable is added and annotated to indicate that this is not a field property

@TableField(exist = false)
private List<Role> authorities;
Copy the code

To implement the GrantedAuthority interface on a Role, all you need is the permission name

public class Role implements Serializable.GrantedAuthority {

    private static final long serialVersionUID = 1L;

    private String name;

    @Override
    public String toString(a) {
        return name;
    }

    @Override
    public String getAuthority(a) {
        returnname; }}Copy the code

Added method in RoleMapper. Java to query owning roles by user ID

   @Select("select name from role r INNER JOIN user_role ur on ur.user_id=1 and ur.role_id=r.id")
    List<Role> selectByUserId(Integer id);
Copy the code

test

The test method is the same as in part 1, which returns the following when retrieving the token

Project address: github.com/zcsherrydc/…

Reference links:

www.cnblogs.com/fp2952/p/89…

Juejin. Cn/post / 684490… For more posts, see zheyday.github. IO /