SpringCloud-OAuth2登陸流程全套

搭建一個oauth2服務器,包括認證、授權和資源服務器

項目地址:https://github.com/zheyday/OAuth2

本文分爲兩個部分

  • 第一部分比較簡單,將客戶端信息和用戶信息固定在程序裏,令牌存儲在內存中
  • 第二部分從數據庫讀取用戶信息,使用jwt生成令牌

一、簡化版

使用Spring Initializr新建項目,勾選如下三個選項

file

pom.xml

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

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

配置Spring Security

新建類WebSecurityConfig 繼承 WebSecurityConfigurerAdapter,並添加@Configuration @EnableWebSecurity註解,重寫三個方法,代碼如下,詳細講解在代碼下面

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

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

    }


    /**
     * 配置了默認表單登陸以及禁用了 csrf 功能,並開啓了httpBasic 認證
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http    // 配置登陸頁/login並允許訪問
                .formLogin().permitAll()
                // 登出頁
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // 其餘所有請求全部需要鑑權認證
                .and().authorizeRequests().anyRequest().authenticated()
                // 由於使用的是JWT,我們這裏不需要csrf
                .and().csrf().disable();
    }

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

主要講解一下

protected void configure(AuthenticationManagerBuilder auth) throws Exception

這個方法是用來驗證用戶信息的。將前端輸入的用戶名和密碼與數據庫匹配,如果有這個用戶才能認證成功。我們注入了一個UserServiceDetail,這個service的功能就是驗證。.passwordEncoder(passwordEncoder())是使用加鹽解密。

UserServiceDetail

實現了UserDetailsService接口,所以需要實現唯一的方法

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);
    }
}

這裏先用假參數實現功能,後面添加數據庫

參數s是前端輸入的用戶名,通過該參數查找數據庫,獲取密碼和角色權限,最後將這三個數據封裝到UserDetails接口的實現類中返回。這裏封裝的類可以使用org.springframework.security.core.userdetails.User或者自己實現UserDetails接口。

UserModel

實現UserDetails接口

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() {
        return password;
    }

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

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

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

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

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

新增username、password和authorities,最後一個存儲的是該用戶的權限列表,也就是用戶擁有能夠訪問哪些資源的權限。密碼加鹽處理

配置Oauth2認證服務器

新建配置類AuthorizationServerConfig 繼承 AuthorizationServerConfigurerAdapter,並添加@Configuration
@EnableAuthorizationServer註解表明是一個認證服務器

重寫三個函數

  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務,客戶端詳情信息在這裏進行初始化,你能夠把客戶端詳情信息寫死在這裏或者是通過數據庫來存儲調取詳情信息。客戶端就是指第三方應用
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點(Token Endpoint)的安全約束.
  • AuthorizationServerEndpointsConfigurer:用來配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
	//從WebSecurityConfig加載
    @Autowired
    private AuthenticationManager authenticationManager;
    //內存存儲令牌
    private TokenStore tokenStore = new InMemoryTokenStore();

    /**
     * 配置客戶端詳細信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            	//客戶端ID
                .withClient("zcs")
                .secret(new BCryptPasswordEncoder().encode("zcs"))
                //權限範圍
                .scopes("app")
            	//授權碼模式
                .authorizedGrantTypes("authorization_code")
                //隨便寫
                .redirectUris("www.baidu.com");
//        clients.withClientDetails(new JdbcClientDetailsService(dataSource));
    }

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

    /**
     * 在令牌端點定義安全約束
     * 允許表單驗證,瀏覽器直接發送post請求即可獲取tocken
     * 這部分寫這樣就行
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 開啓/oauth/token_key驗證端口無權限訪問
                .tokenKeyAccess("permitAll()")
                // 開啓/oauth/check_token驗證端口認證權限訪問
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }
}

客戶端詳細信息同樣也是測試用,後續會加上數據庫。令牌服務暫時是用內存存儲,後續加上jwt。

先實現功能最重要,複雜的東西一步步往上加。

配置資源服務器

資源服務器也就是服務程序,是需要訪問的服務器

新建ResourceServerConfig繼承ResourceServerConfigurerAdapter

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
//                antMatcher表示只能處理/user的請求
                .antMatcher("/user/**")
                .authorizeRequests()
                .antMatchers("/user/test1").permitAll()
                .antMatchers("/user/test2").authenticated()
//                .antMatchers("user/test2").hasRole("USER")
//                .anyRequest().authenticated()
        ;
    }
}

ResourceServerConfigurerAdapter的Order默認值是3,小於WebSecurityConfigurerAdapter,值越小優先級越大

關於ResourceServerConfigurerAdapterWebSecurityConfigurerAdapter的詳細說明見

https://www.jianshu.com/p/fe1194ca8ecd

新建UserController

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

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

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

}

測試

  1. 獲取code
    瀏覽器訪問http://127.0.0.1:9120/oauth/authorize?client_id=zcs&response_type=code&redirect_uri=www.baidu.com,然後跳出登陸頁面,

file
認證

file

地址欄會出現回調頁面,並且帶有code參數 http://127.0.0.1:9120/oauth/www.baidu.com?code=FGQ1jg

  1. 獲取token
    postman訪問http://127.0.0.1:9120/oauth/token?code=FGQ1jg&grant_type=authorization_code&redirect_uri=www.baidu.com&client_id=zcs&client_secret=zcs,code填寫剛纔得到的code,使用POST請求
    file
  2. 訪問資源
    /user/test2是受保護資源,我們通過令牌訪問
    file

二、升級版

JWT

有很多人會把JWT和OAuth2來作比較,其實它倆是完全不同的概念,沒有可比性。

JWT是一種認證協議,提供一種用於發佈接入令牌、並對發佈的簽名接入令牌進行驗證的方法。

OAuth2是一種授權框架,提供一套詳細的授權機制。

Spring Cloud OAuth2集成了JWT作爲令牌管理,因此使用起來很方便

JwtAccessTokenConverter是用來生成token的轉換器,而token令牌默認是有簽名的,且資源服務器需要驗證這個簽名。此處的加密及驗籤包括兩種方式:
對稱加密、非對稱加密(公鑰密鑰)
對稱加密需要授權服務器和資源服務器存儲同一key值,而非對稱加密可使用密鑰加密,暴露公鑰給資源服務器驗籤,本文中使用非對稱加密方式。

通過jdk工具生成jks證書,通過cmd進入jdk安裝目錄的bin下,運行命令

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

會在當前目錄生成oauth2.jks文件,放入resource目錄下。

maven默認不加載resource目錄下的文件,所以需要在pom.xml中配置,在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>

在原來的AuthorizationServerConfig中更改部分代碼

	@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() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 非對稱加密算法對token進行簽名
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        final JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
        // 導入證書
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "mypass".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
        return converter;
    }

jwtAccessTokenConverter方法中有一個CustomJwtAccessTokenConverter類,這是繼承了JwtAccessTokenConverter,自定義添加了額外的token信息

/**
 * 自定義添加額外token信息
 */
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);
    }
}

Security

之前登陸是用假數據,現在通過連接數據庫進行驗證。

建立三個表,user存儲用戶賬號和密碼,role存儲角色,user_role存儲用戶的角色

file

user表

file

role表
file

user_role表

file
使用mybatis-plus生成代碼,改造之前的UserServiceDetailUserModel

file

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("用戶名或密碼錯誤");
        }

        user.setAuthorities(roleMapper.selectByUserId(user.getId()));
        return user;
    }
}

通過UserMapper查詢用戶信息,然後封裝到User中,在自動生成的User上實現UserDetails接口

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() {
    }

    public Integer getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        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() {
        return true;
    }

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

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

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

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


    @Override
    public String toString() {
        return "User{"  
                "id="   id  
                ", username="   username  
                ", password="   password  
                "}";
    }
}

解釋說明:

UserDetails中需要重寫一個方法,是存儲用戶權限的

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

所以新增了一個變量,並且打上註解表示這不是一個字段屬性

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

在Role上實現GrantedAuthority接口,只需要權限名稱就可以了

public class Role implements Serializable, GrantedAuthority {

    private static final long serialVersionUID = 1L;

    private String name;

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

    @Override
    public String getAuthority() {
        return name;
    }
}

在RoleMapper.java中新增方法,通過用戶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);

測試

測試方法和第一部分一樣,獲取令牌的時候返回如下

file

參考鏈接

https://www.cnblogs.com/fp2952/p/8973613.html

https://juejin.im/post/5c5ae6566fb9a049b3486e38

更多文章見個人博客 https://zheyday.github.io/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章