4 months ago

來補一下怎麼保護 ResourceServer

套件依賴

build.gradle
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 是我們需另外加進來的

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

Account.java
@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;
}

角色

Role.java
@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

OAuth2ResourceServerConfig.java
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 這邊配置需要的權限

RoleService.java
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 的功能,你其實可以像下面這樣設定

AccountRepository.java
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 內部所有的方法都不對外露出,就可以放心用啦

AccountPrivateRepository.java
@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);
}

啟動程式

PsAccountApplication.java
@EnableJpaAuditing
//@EnableTransactionManagement

@SpringBootApplication
public class PsAccountApplication {

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

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

← spring-retry 使用方式 開發工具發生找不到套件問題 →
 
comments powered by Disqus