iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 13
0
Software Development

30天從零開始 使用 Spring Boot 跟 Spring Cloud 建構完整微服務架構系列 第 13

Day 13 - 使用 Spring Security 驗證 OAuth 授權

說明一下怎麼保護 ResourceServer

套件依賴

buildscript {
    ext {
        springBootVersion = '1.5.2.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'ps-account'
    version = '0.0.1-SNAPSHOT'
}

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-mail')
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile 'org.springframework.security.oauth:spring-security-oauth2:2.0.12.RELEASE'
    compile 'org.springframework.security:spring-security-jwt:1.0.7.RELEASE'
    compile 'org.modelmapper:modelmapper:0.7.7'
    compile 'org.apache.commons:commons-lang3:3.5'
    compile 'mysql:mysql-connector-java:6.0.5'
    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.6.1'
    compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.6.1'
    compile group: 'io.springfox', name: 'springfox-data-rest', version: '2.6.1'
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

spring-security-oauth2 spring-security-jw 是我們需另外加進來的

我們會有一些基礎的帳號或是角色的物件
帳號

@Data
@Entity
@EntityListeners(AuditingEntityListener.class) //加這行 CreatedBy 才會生效
public class Account {
    @Id
    //@GeneratedValue
    private String accountid;

    @NotNull
    @UniqueUsername(message = "Username already exists")
    @Size(min = 5, max = 255, message = "Username have to be grater than 8 characters")
    @Column(unique = true)
    private String username;

    private String email;

    @JsonIgnore
    @NotNull
    @Size(min = 8, max = 255, message = "Password have to be grater than 8 characters")
    private String password;

    @NotNull
    private boolean enabled = true;

    @NotNull
    private boolean credentialsexpired = false;

    @NotNull
    private boolean expired = false;

    @NotNull
    private boolean locked = false;

//    @Formula("(select * from Role r where r.roleid in (select roleid from account_role ar where ar.accountid = accountid ))")
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "AccountRole", joinColumns = @JoinColumn(name = "accountid", referencedColumnName = "accountid", insertable = false, updatable = false),
            inverseJoinColumns = @JoinColumn(name = "roleid", referencedColumnName = "roleid", insertable = false, updatable = false) )
    private List<Role> roles;

    @CreatedDate
    @Column(name = "createddate")
    private Date createddate;
    @CreatedBy
    @Column(name = "createdby")
    private String createdby;
    @LastModifiedDate
    @Column(name = "lastmodifieddate")
    private Date lastmodifieddate;
    @LastModifiedBy
    @Column(name = "lastmodifiedby")
    private String lastmodifiedby;
}

角色

@Data
@Entity
@EntityListeners(AuditingEntityListener.class) //加這行 CreatedBy 才會生效
public class Role {
    @Id
    private String roleid;
    @NotNull
    private String code;
    @NotNull
    private String label;
    @CreatedDate
    @Column(name = "createddate")
    private Date createddate;
    @CreatedBy
    @Column(name = "createdby")
    private String createdby;
    @LastModifiedDate
    @Column(name = "lastmodifieddate")
    private Date lastmodifieddate;
    @LastModifiedBy
    @Column(name = "lastmodifiedby")
    private String lastmodifiedby;
}

配置 ResourceServer Security

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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;

/**
 * Created by samchu on 2017/2/17.
 */
@Configuration
@EnableResourceServer
// prePostEnabled 很重要,可以讓你配置 @PreAuthorize("#oauth2.hasScope('account')") 在任何需要的方法上
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

    // 這邊配置這服務的 resourceId ,當 jwt 中不含符合的 resourceId 則會拒絕操作
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("account");
        resources.tokenServices(tokenServices());
    }

    // 配置哪些資源可以不用檢驗 Token ,這邊是對 swagger 的檔案無條件存取 及忘記密碼 passwordforget,其他的都要
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.httpBasic().disable();
        http.authorizeRequests().antMatchers(
                "/swagger-ui.html",
                "/v2/api-docs/**",
                "/swagger-resources/**",
                "/webjars/**",
                "/api/v1/passwordforget"
        ).permitAll();
        // 或是也可以集中配置在這裡
        //.antMatchers(HttpMethod.GET, "/my").access("#oauth2.hasScope('my-resource.read')")
        http.authorizeRequests().anyRequest().fullyAuthenticated();
    }

    // 主要是配置 JWT 的密鑰
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("ASDFASFsdfsdfsdfsfadsf234asdfasfdas");
        return converter;
    }

    // 配置 JwtTokenStore
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    // 配置,使用預設的 DefaultTokenServices 就可以了,因為我們這邊只是做驗證而已
    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        return defaultTokenServices;
    }
}

接下來就可以在 Service 這邊配置需要的權限

import com.ps.dto.RoleDto;
import com.ps.model.Role;
import com.ps.repository.RoleRepository;
import org.apache.commons.lang3.RandomStringUtils;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import java.util.List;

/**
 * Created by samchu on 2017/2/17.
 */
@Service
public class RoleService {

    @Autowired
    private RoleRepository roleRepository;

    // 這邊需要有 role 或是 role.readonly 的操作範圍的人才可以讀取角色列表
    @PreAuthorize("#oauth2.hasScope('role') or #oauth2.hasScope('role.readonly')")
    public List<Role> listAll(){
        return roleRepository.findAll();
    }

    // 這邊是寫入角色,所以限定 role 的操作範圍才可以寫入
    @PreAuthorize("#oauth2.hasScope('role')")
    public Role create(RoleDto roleDto) {
        ModelMapper modelMapper = new ModelMapper();
        Role role = modelMapper.map(roleDto, Role.class);
        role.setRoleid(RandomStringUtils.randomAlphanumeric(10));
        roleRepository.save(role);
        return role;
    }
}

像是 hasScope('role') 或是 hasScope('role.readonly') 其實都是非常簡單好讀的敘述方式

那像用到 spring-boot-starter-data-rest 的功能,你其實可以像下面這樣設定

import com.ps.model.Account;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.security.access.prepost.PreAuthorize;

/**
 * Created by samchu on 2017/2/9.
 */
@RepositoryRestResource
public interface AccountRepository extends JpaRepository<Account, String> {

    //@RestResource(path = "findByUsername")
    @Query("SELECT a FROM Account a WHERE a.username = :username")
    Account findByUsername(@Param("username") String username);

    // Prevents GET /accounts/:id
    @Override
    @RestResource(exported = true)
    @PreAuthorize("#oauth2.hasScope('account')")
    Account findOne(String id);

    // Prevents GET /account
    @Override
    @RestResource(exported = false)
    @PreAuthorize("#oauth2.hasScope('account')")
    Page<Account> findAll(Pageable pageable);

    // Prevents POST /account and PATCH /account/:id
    @Override
    @RestResource(exported = true)
    @PreAuthorize("#oauth2.hasScope('account')")
    Account save(Account s);

    // Prevents DELETE /account/:id
    @Override
    @RestResource(exported = false)
    @PreAuthorize("#oauth2.hasScope('account')")
    void delete(Account t);
}
  1. 透過 RestResource(exported = false) 來配置此方法是否供外部透過 Rest 操作
  2. 特過 PreAuthorize 一樣可以設定符合操作的 scope 範圍

RestResource 預設是 exported = true,所以有的時候你自己會卡到外部內部權限問題
舉例說外部 rest 操作必須要有 account 操作範圍,但是某些時候是系統內部要操作,這時候你就沒有 token 來檢驗
不過你可以另外設定個系統操作的 Repository 像下面,這樣一來這個 Class 內部所有的方法都不對外露出,就可以放心用啦

@Repository
@RestResource(exported = false)
public interface AccountPrivateRepository extends JpaRepository<Account, String> {

    @Query("SELECT a FROM Account a WHERE a.username = :username")
    Account findByUsername(@Param("username") String username);
}

啟動程式

@EnableJpaAuditing
//@EnableTransactionManagement
@SpringBootApplication
public class PsAccountApplication {

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

透過標準的 OAuth 跟 JWT 是不是省下很多自己開發的力氣呢?
使用 OAuth 不論是自用或開放兩相宜XD,使用 JWT 簡化驗證架構跟流程
雖然學習是有成本的,但是比起自己做一套授權系統,應該還是划算的很多
把力氣留在你的商業平台吧,這種基礎建設就盡量使用開源以及標準吧


上一篇
Day 12 - 使用 Spring Security 建置 OAuth2 授權服務
下一篇
Day 14 - 介紹 Spring Cloud
系列文
30天從零開始 使用 Spring Boot 跟 Spring Cloud 建構完整微服務架構35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言