Spring Cloud:認證 授權 OAuth2、JWT

OAuth2

OAuth2是當前授權的行業標準,其重點在於爲Web應用程序、桌面應用程序、移動設備以及室內設備的授權流程提供簡單的客戶端開發方式。它爲第三方應用提供對HTTP服務的有限訪問,既可以是資源擁有者通過授權允許第三方應用獲取HTTP服務,也可以是第三方以自己的名義獲取訪問權限。

角色

OAuth2中主要分爲了4種角色:

  • Resource Owner(資源所有者),是能夠對受保護的資源授予訪問權限的實體,可以是一個用戶,這時會稱爲終端用戶(end-user)。
  • Resource Server(資源服務器),持有受保護的資源,允許持有訪問令牌(Access Token)的請求訪問受保護資源。
  • Client(客戶端),持有資源所有者的授權,代表資源所有者對受保護資源進行訪問。
  • Authorization Server(授權服務器),對資源所有者的授權進行認證,成功後向客戶端發送訪問令牌。

很多時候,資源服務器和授權服務器是合二爲一的,在授權交互的時候作爲授權服務器,在請求資源交互時作爲資源服務器

Resource Server的配置

Resource Server(可以是授權服務器,也可以是其他的資源服務)提供了受OAuth2保護的資源,這些資源爲API接口、Html頁面、Js文件等.Spring OAuth2提供了實現此保護功能的Spring Security認證過濾器。在加了@Configuration註解的配置類上加@EnableResourceServer註解,開啓Resource Server的功能

JWT

JSON Web Token(JWT)是一種開放的標準(RFC 7519),JWT定義了一種緊湊且自包含的標準,該標準旨在將各個主體的信息包裝爲JSON對象。主體信息是通過數字簽名進行加密和驗證的。常使用HMAC算法或RSA(公鑰/私鑰的非對稱性加密)算法對JWT進行簽名,安全性很高。

  • 緊湊性(compact):由於是加密後的字符串,JWT數據體積非常小,可通過POST請求參數或HTTP請求頭髮送。另外,數據體積小意味着傳輸速度很快。
  • 自包含(self-contained):JWT包含了主體的所有信息,所以避免了每個請求都需要向Uaa服務驗證身份,降低了服務器的負載。

JWT由3個部分組成,分別以“.”分隔,組成部分如下。

  • Header(頭):Header通常由兩部分組成:令牌的類型(即JWT)和使用的算法類型,如HMAC、SHA256和RSA
  • Payload(有效載荷):了用戶的一些信息和Claim(聲明、權利)。有3種類型的Claim:保留、公開和私人
  • Signature(簽名):需要將Base64編碼後的Header、Payload和密鑰進行簽名

uaa配置

1. 依賴:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
    runtimeOnly 'mysql:mysql-connector-java'

2. 配置文件:

bootstrap.yml:

spring:
  application:
    name: consul-auth
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}


management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

application.yml:

server:
  port: 8731

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123zxc

  main:
    allow-bean-definition-overriding: true

3. 安裝mysql數據庫

需要通過數據庫進行對人員登錄人員數據進行存儲這裏使用了Mysql數據庫

使用docker安裝:

    mysql:
      image: mysql
      networks:
        - spring
      restart: always
      ports:
        - 33060:33060
        - 3306:3306
      volumes:
        - ./mysql/db:/var/lib/mysql
        - ./mysql/conf.d:/etc/mysql/conf.d
      environment:
        - MYSQL_ROOT_PASSWORD=123zxc    
      command: --default-authentication-plugin=mysql_native_password

創建一個user的表用來存儲人員數據:

CREATE TABLE `myUser` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

BEGIN;
INSERT INTO `myUser` VALUES ('1', '$2a$10$uuFQKbr2q/8aqYlPEBlRw.Z9UrtEPrydIh7IUXaEGVWBowY8mZrUq', 'ffzs'),('2', '$2a$10$QgQ9OtiCMnGzYGPabDzOkeBda0Sb8wqzwnTSErJWPx4GfeNOUvh7q', 'sleepycat');
COMMIT;

添加兩個用戶

4. AuthorizationServer配置

/**
 * @author ffzs
 * @describe
 * @date 2020/6/7
 */

@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsServiceImpl userServiceDetail;

    /**
     * 配置客戶端信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.inMemory()
                .withClient("consul_server")  // 用戶端id 需要在Authorization Server中是唯一的。
                .secret("123456") // 連接密碼
                .scopes("server")  // 配置的客戶端域爲 service
//                .autoApprove(true)   // client_secret
//                .authorities("ROLE_ADMIN", "ROLE_USER")  // 權限信息
                .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code", "client_credentials")  // 類驗證類型
                .accessTokenValiditySeconds(60*60);  //失效時間
    }

    /**
     * 配置授權Token的節點和Token服務
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //只有配置了該選項,密碼認證纔會開啓。在大多數情況下都是密碼驗證,所以一般都會配置這個選項
                /**
                 * 需要設置Token的管理策略,目前支持以下3種:
                 * InMemoryTokenStore:Token存儲在內存中。
                 * JdbcTokenStore:Token存儲在數據庫中。需要引入spring-jdbc的依賴包,並配置數據源,以及初始化Spring OAuth2的數據庫腳本
                 * JwtTokenStore:採用JWT形式,這種形式沒有做任何的存儲,因爲JWT本身包含了用戶驗證的所有信息,不需要存儲。採用這種形式,需要引入spring-jwt的依賴
                 */
                .tokenStore(jwtTokenStore())
                .tokenEnhancer(jwtTokenEnhancer())
//                .userDetailsService(userServiceDetail)  // 配置獲取用戶認證信息的接口
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

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

    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("fanfanzhisu");
        return converter;
    }

    /**
     * Token 節點的安全策略
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
}

5. WebSecurity 配置

  • 開放了actuator路徑供健康檢查
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userServiceDetail;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/**").authenticated()
                .and()
                .httpBasic();
    }
//
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userServiceDetail)
                .passwordEncoder(new BCryptPasswordEncoder());
    }


    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
}

Mybatis文件配置:

配置一下model,dao等文件路徑:

@Configuration
@MapperScan("com.ffzs.consulauth.**.dao")
public class MybatisConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setDataSource(dataSource);
        ssfb.setTypeAliasesPackage("com.ffzs.consulauth.**.model");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));

        return ssfb.getObject();
    }
}

model,dao等文件通過idea上的generator插件生成即可。
在這裏插入圖片描述

重新寫一下獲取token時匹配用戶部分邏輯

UserDetailsServiceImpl.class

  • 管理員給ROLE_ADMIN, ROLE_USER 權限
  • 普通用戶只給 ROLE_USER 權限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired(required = false)
    private MyUserDao myUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser myUser = myUserDao.findByUsername(username);
        System.out.println(username);
        if (myUser == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        if (username.equals("admin") || username.equals("ffzs")){
            return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN, ROLE_USER"));
        }
        return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

userdao要添加一個通過名字獲取信息的方法:

在這裏插入圖片描述

在這裏插入圖片描述

啓動類更改:

添加==@EnableDiscoveryClient==即可

在這裏插入圖片描述

運行&&測試

consul上註冊成功
在這裏插入圖片描述

使用postman發送post請求:

http://localhost:8731/oauth/token?client_id=consul_server&client_secret=123456&grant_type=password&username=ffzs&password=123zxc

成功獲取token

在這裏插入圖片描述

post請求中的參數及描述:
在這裏插入圖片描述

service配置

創建一個consul-service項目,用於提供登錄,註冊等服務:

1. 依賴:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:2.2.2.RELEASE'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
    implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
    runtimeOnly 'mysql:mysql-connector-java'

2. 配置文件

bootstrap.yml:

spring:
  application:
    name: consul-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}

feign:
  httpclient:
    enabled: true

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

application.yml:

server:
  port: 8777

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123zxc

logging:
  level:
    root: INFO
    org.springframework.web: INFO
    org.springframework.security: INFO
    org.springframework.security.oauth2: INFO

3. Mybatis配置

dao,model跟uaa的一樣

mybatis的配置文件跟uaa的也基本相同:

@Configuration
@MapperScan("com.ffzs.consulservice.**.dao")
public class MybatisConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setDataSource(dataSource);
        ssfb.setTypeAliasesPackage("com.ffzs.consulservice.**.model");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));

        return ssfb.getObject();
    }
}

4. Resource配置

ResourceConfiguration.class:

  • 配置了兩個測試路徑,user路徑用於登錄,不做權限限制
  • /hello/admin", "/hello/header只右ADMIN權限才能訪問
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/user/**").permitAll()
                .antMatchers("/hello/user", "hello/test").hasRole("USER")
                .antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
                .antMatchers("/**").authenticated();
    }

    /**
     * tokenServices 定義Token Service 用ResourceServerTokenservices類,配置Token是如何編碼和解碼的 可以用RemoteTokenServices類,即Resource Server採用遠程授權服務器進行Token解碼,這時也不需要配置此選項
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId("server")  // 配置資源Id。
                .tokenStore(jwtTokenStore());
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("fanfanzhisu");
        return converter;
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtTokenConverter());
    }
}

5. 註冊功能

添加一個功能用於註冊,service

    public int insertUser(String username, String  password){
        MyUser user=myUserDao.findByUsername(username);
        if (user != null){
            user.setPassword(encoder.encode(password));
            return myUserDao.updateByPrimaryKeySelective(user);
        }else{
            MyUser myUser=new MyUser();
            myUser.setUsername(username);
            myUser.setPassword(encoder.encode(password));
            return myUserDao.insertSelective(myUser);
        }
    }

controller:

    @PostMapping("/register")
    public String postUser(@RequestParam("username") String username , @RequestParam("password") String password){
        int back = userServiceDetail.insertUser(username,password);
        return back == 1?"註冊成功":"註冊失敗";
    }

6. 登錄功能

編寫一個service通過feign向註冊中心的consul-auth,也就是上面uaa發送登錄請求並獲取token

@FeignClient(value = "consul-auth")
public interface AuthServiceClient {

    @PostMapping(value = "/oauth/token")
    MyToken getToken(@RequestHeader("Content-Type") String content, @RequestParam("client_id") String client_id, @RequestParam("client_secret") String client_secret, @RequestParam("grant_type") String type,
                     @RequestParam("username") String username, @RequestParam("password") String password);

}

調用getToken獲取token,之後裝入userLoginDTO中:

    public UserLoginDTO login(String username, String password){
        MyUser user=myUserDao.findByUsername(username);
        if (null == user) {
            throw new RuntimeException("error username");
        }
        if(!encoder.matches(password,user.getPassword())){
            throw new RuntimeException("error password");
        }

        MyToken myToken =authServiceClient.getToken("application/json", "consul_server","123456","password", username, password);

        if(myToken ==null){
            throw new RuntimeException("error internal");
        }

        UserLoginDTO userLoginDTO=new UserLoginDTO();
        userLoginDTO.setMyToken(myToken);
        userLoginDTO.setUser(user);
        return userLoginDTO;
    }

用於登入的controller:

    @PostMapping("/login")
    public UserLoginDTO login(@RequestParam("username") String username , @RequestParam("password") String password){
        return userServiceDetail.login(username,password);
    }

7. 測試登錄

測試註冊功能,訪問http://localhost:8777/user/register?username=xiaozhang&password=123456,結果如下:

在這裏插入圖片描述

查看數據庫,xiaozhang的用戶信息已經存入數據庫:

在這裏插入圖片描述

測試登入功能,http://localhost:8777/user/login?username=xiaozhang&password=123456 ,返回token說明登錄成功:

在這裏插入圖片描述

8. 測試權限

因爲一些業務的需求會有一些端口需要鑑權,編寫用於測試的controller:

@RestController
@RequestMapping("hello")
public class TestController {

    @GetMapping("user")
    public String user(){
        return "hello!!! 普通用戶 !!!";
    }

    @GetMapping("admin")
    public String admin(){
        return "hello!!! 權限dog !!!!";
    }

    @GetMapping("role")
    public String test() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        StringBuilder res = new StringBuilder()
                .append("用戶名: ").append(authentication.getName()).append("\n")
                .append("權限情況: ");
        for (Object it :authentication.getAuthorities().toArray()) {
            res.append(it).append("\t");
        }
        return res.toString();
    }

    @RequestMapping("header")
    public String header(HttpServletRequest request) {
        StringBuilder html = new StringBuilder("<table border='2' cellspacing='0'>");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            html.append("<tr><td>").append(key).append("</td><td>").append(request.getHeader(key)).append("</td></tr>");
        }
        html.append("</table>");
        return html.toString();
    }
}

幾個端口權限設置如下:

                .antMatchers("/hello/user", "hello/test").hasRole("USER")
                .antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
  • USER 權限可以訪問 “/hello/user”, “hello/test”
  • ADMIN 都可以訪問

設置token,使用postman可以直接設置,如下圖,不使用postman的話可以寫道header裏:

在這裏插入圖片描述

這時我們訪問,http://localhost:8777/hello/user,可以訪問user;

在這裏插入圖片描述

訪問,http://localhost:8777/hello/admin,因爲沒有權限,限制訪問:

在這裏插入圖片描述

訪問,http://localhost:8777/hello/role,這裏我們可以看到xiaozhang只有ROLU_USER的權限:

在這裏插入圖片描述

更換用戶,使用ffzs賬號登錄,更換獲得token,權限都token都長了:

在這裏插入圖片描述

使用ffzs的賬號訪問http://localhost:8777/hello/role

在這裏插入圖片描述

ROLE_ADMIN ROLE_USER同時擁有兩個權限:

這時我們試試用這個賬號能否訪問http://localhost:8777/hello/admin

在這裏插入圖片描述

有了ADMIN權限可以正常訪問了。

看一下header,訪問 http://localhost:8777/hello/header

在這裏插入圖片描述

可見token在header裏的形式,如果不容postman發送請求就添加通過將"authorization":"Bearer token"添加到你的header中就可以訪問了

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