說明一下怎麼保護 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);
}
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 簡化驗證架構跟流程
雖然學習是有成本的,但是比起自己做一套授權系統,應該還是划算的很多
把力氣留在你的商業平台吧,這種基礎建設就盡量使用開源以及標準吧