diff --git a/sample-tom/src/main/resources/application.yml b/sample-tom/src/main/resources/application.yml index 981543e5..2a41527d 100644 --- a/sample-tom/src/main/resources/application.yml +++ b/sample-tom/src/main/resources/application.yml @@ -41,4 +41,4 @@ spring: initialization-mode: always jpa: database: mysql - show-sql: true \ No newline at end of file + show-sql: true diff --git a/samples/pom.xml b/samples/pom.xml index 1614c943..16ba2592 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -25,6 +25,8 @@ solon-plugin-sureness-demo spring-gateway-sureness zuul-sureness + shenyu-sureness + shenyu-sureness-sql sureness-spring-boot-starter-example micro-profile-sureness @@ -38,4 +40,4 @@ - \ No newline at end of file + diff --git a/samples/shenyu-service/pom.xml b/samples/shenyu-service/pom.xml new file mode 100644 index 00000000..1707d6a7 --- /dev/null +++ b/samples/shenyu-service/pom.xml @@ -0,0 +1,55 @@ + + + + samples + com.usthe.sureness + 1.0.0-SNAPSHOT + + 4.0.0 + + shenyu-service + + + 8 + 8 + + + + org.springframework.boot + spring-boot-starter-webflux + 2.2.2.RELEASE + + + org.springframework.boot + spring-boot-starter-actuator + 2.2.2.RELEASE + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.dromara + soul-spring-boot-starter-gateway + 2.3.0-RELEASE + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.dromara + soul-spring-boot-starter-sync-data-websocket + 2.3.0-RELEASE + + + + \ No newline at end of file diff --git a/samples/shenyu-service/src/main/java/com/shenyu/AppBootStrap.java b/samples/shenyu-service/src/main/java/com/shenyu/AppBootStrap.java new file mode 100644 index 00000000..5d5fa094 --- /dev/null +++ b/samples/shenyu-service/src/main/java/com/shenyu/AppBootStrap.java @@ -0,0 +1,18 @@ +package com.shenyu; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @title: AppBootStrap + * @Author hqgordon + * @Date: 2021/8/3 3:15 下午 + * @Description: + * @Version 1.0 + */ +@SpringBootApplication +public class AppBootStrap { + public static void main(String[] args) { + SpringApplication.run(AppBootStrap.class,args); + } +} \ No newline at end of file diff --git a/samples/shenyu-service/src/main/resources/application.yml b/samples/shenyu-service/src/main/resources/application.yml new file mode 100644 index 00000000..f71bec8b --- /dev/null +++ b/samples/shenyu-service/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + main: + allow-bean-definition-overriding: true +management: + health: + defaults: + enabled: false +shenyu: + sync: + websocket: + urls: ws://localhost:9095/websocket +server: + port: 9196 \ No newline at end of file diff --git a/samples/shenyu-sureness-sql/pom.xml b/samples/shenyu-sureness-sql/pom.xml new file mode 100644 index 00000000..34772f67 --- /dev/null +++ b/samples/shenyu-sureness-sql/pom.xml @@ -0,0 +1,108 @@ + + + + samples + com.usthe.sureness + 1.0.0-SNAPSHOT + + 4.0.0 + + shenyu-sureness-sql + + + 13 + 13 + + + + org.yaml + snakeyaml + 1.25 + + + com.usthe.sureness + sureness-core + 1.0.3 + + + org.springframework.boot + spring-boot-starter-webflux + 2.2.2.RELEASE + + + org.springframework.boot + spring-boot-starter-actuator + 2.2.2.RELEASE + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.dromara + soul-spring-boot-starter-gateway + 2.3.0-RELEASE + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.dromara + soul-spring-boot-starter-sync-data-websocket + 2.3.0-RELEASE + + + org.dromara + soul-spring-boot-starter-plugin-httpclient + 2.3.0-RELEASE + + + org.dromara + soul-spring-boot-starter-plugin-divide + 2.3.0-RELEASE + + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + 2.2.2.RELEASE + + + + org.springframework.boot + spring-boot-starter-data-jpa + 2.2.2.RELEASE + + + + mysql + mysql-connector-java + 5.1.47 + + + + + + org.projectlombok + lombok + 1.18.6 + provided + + + + diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/AppBootSql.java b/samples/shenyu-sureness-sql/src/main/java/sureness/AppBootSql.java new file mode 100644 index 00000000..5fe1023d --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/AppBootSql.java @@ -0,0 +1,18 @@ +package sureness; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @title: AppBootSql + * @Author hqgordon + * @Date: 2021/8/5 9:41 上午 + * @Description: + * @Version 1.0 + */ +@SpringBootApplication +public class AppBootSql { + public static void main(String[] args) { + SpringApplication.run(AppBootSql.class,args); + } +} \ No newline at end of file diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/controller/AccountController.java b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/AccountController.java new file mode 100644 index 00000000..7b2fee9a --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/AccountController.java @@ -0,0 +1,98 @@ +package sureness.controller; + + +import com.usthe.sureness.util.JsonWebTokenUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sureness.pojo.dto.Account; +import sureness.pojo.dto.Message; +import sureness.service.AccountService; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * @author tomsun28 + * @date 00:24 2019-08-01 + */ +@RestController +@RequestMapping("/auth") +@Slf4j +public class AccountController { + + @Autowired + private AccountService accountService; + + private static final String TOKEN_SPLIT = "--"; + + @PostMapping("/token") + public ResponseEntity issueJwtToken(@RequestBody @Validated Account account) { + boolean authenticatedFlag = accountService.authenticateAccount(account); + if (!authenticatedFlag) { + Message message = Message.builder() + .errorMsg("username or password not incorrect").build(); + if (log.isDebugEnabled()) { + log.debug("account: {} authenticated fail", account); + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(message); + } + List ownRole = accountService.loadAccountRoles(account.getUsername()); + String jwt = JsonWebTokenUtil.issueJwt(UUID.randomUUID().toString(), account.getUsername(), + "tom-auth-server", 3600L, ownRole); + Map responseData = Collections.singletonMap("token", jwt); + Message message = Message.builder().data(responseData).build(); + if (log.isDebugEnabled()) { + log.debug("issue token success, account: {} -- token: {}", account, jwt); + } + return ResponseEntity.status(HttpStatus.CREATED).body(message); + } + + @PostMapping("/custom/token") + public ResponseEntity issueCustomToken(@RequestBody @Validated Account account) { + boolean authenticatedFlag = accountService.authenticateAccount(account); + if (!authenticatedFlag) { + Message message = Message.builder() + .errorMsg("username or password not incorrect").build(); + if (log.isDebugEnabled()) { + log.debug("account: {} authenticated fail", account); + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(message); + } + long refreshPeriodTime = 36000L; + String token = account.getUsername() + TOKEN_SPLIT + System.currentTimeMillis() + + TOKEN_SPLIT + refreshPeriodTime + + TOKEN_SPLIT + UUID.randomUUID().toString().replace("-", ""); + TokenStorage.addToken(account.getUsername(), token); + Map responseData = Collections.singletonMap("customToken", token); + Message message = Message.builder().data(responseData).build(); + if (log.isDebugEnabled()) { + log.debug("issue token success, account: {} -- token: {}", account, token); + } + return ResponseEntity.status(HttpStatus.CREATED).body(message); + } + + @PostMapping("/register") + public ResponseEntity accountRegister(@RequestBody @Validated Account account) { + if (accountService.registerAccount(account)) { + Map responseData = Collections.singletonMap("success", "sign up success, login after"); + Message message = Message.builder().data(responseData).build(); + if (log.isDebugEnabled()) { + log.debug("account: {}, sign up success", account); + } + return ResponseEntity.status(HttpStatus.CREATED).body(message); + } else { + Message message = Message.builder() + .errorMsg("username already exist").build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/controller/AnnotationController.java b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/AnnotationController.java new file mode 100644 index 00000000..4bdc54c5 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/AnnotationController.java @@ -0,0 +1,29 @@ +package sureness.controller; + +import com.usthe.sureness.provider.annotation.RequiresRoles; +import com.usthe.sureness.provider.annotation.WithoutAuth; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + + +/** + * @author tomsun28 + * @date 2021/1/19 14:53 + */ +@RestController +public class AnnotationController { + + @GetMapping("/api/annotation/source1") + @RequiresRoles(roles = {"role1", "role2", "role3"}, mapping = "/api/annotation/source1", method = "get") + public ResponseEntity api1Mock1() { + return ResponseEntity.ok("success"); + } + + @GetMapping("/api/annotation/source2") + @WithoutAuth(mapping = "/api/annotation/source2", method = "get") + public ResponseEntity api1Mock2() { + return ResponseEntity.ok("success"); + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/controller/ResourceController.java b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/ResourceController.java new file mode 100644 index 00000000..78216746 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/ResourceController.java @@ -0,0 +1,92 @@ +package sureness.controller; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import sureness.pojo.dto.Message; +import sureness.pojo.entity.AuthResourceDO; +import sureness.service.ResourceService; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 00:24 2019-08-01 + */ +@RequestMapping("/resource") +@RestController +@Slf4j +public class ResourceController { + + @Autowired + private ResourceService resourceService; + + @PostMapping + public ResponseEntity addResource(@RequestBody @Validated AuthResourceDO authResource) { + if (resourceService.addResource(authResource)) { + if (log.isDebugEnabled()) { + log.debug("add resource success: {}", authResource); + } + return ResponseEntity.status(HttpStatus.CREATED).build(); + } else { + Message message = Message.builder().errorMsg("resource already exist").build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + } + + @PutMapping + public ResponseEntity updateResource(@RequestBody @Validated AuthResourceDO authResource) { + if (resourceService.updateResource(authResource)) { + if (log.isDebugEnabled()) { + log.debug("update resource success: {}", authResource); + } + return ResponseEntity.ok().build(); + } else { + Message message = Message.builder().errorMsg("resource not exist").build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + } + + @DeleteMapping("/{resourceId}") + public ResponseEntity deleteResource(@PathVariable @NotBlank Long resourceId ) { + if (resourceService.deleteResource(resourceId)) { + if (log.isDebugEnabled()) { + log.debug("delete resource success: {}", resourceId); + } + return ResponseEntity.ok().build(); + } else { + Message message = Message.builder().errorMsg("delete resource fail, please try again later").build(); + log.error("delete resource fail: {}", resourceId); + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(message); + } + } + + @GetMapping("/{currentPage}/{pageSize}") + public ResponseEntity getResource(@PathVariable Integer currentPage, @PathVariable Integer pageSize ) { + if (Objects.isNull(currentPage) || Objects.isNull(pageSize)) { + // no pageable + Optional> resourceListOptional = resourceService.getAllResource(); + if (resourceListOptional.isPresent()) { + Message message = Message.builder().data(resourceListOptional.get()).build(); + return ResponseEntity.ok().body(message); + } else { + Message message = Message.builder().data(new ArrayList<>(0)).build(); + return ResponseEntity.ok().body(message); + } + } else { + // pageable + Page resourcePage = resourceService.getPageResource(currentPage, pageSize); + Message message = Message.builder().data(resourcePage.get()).build(); + return ResponseEntity.ok().body(message); + } + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/controller/RoleController.java b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/RoleController.java new file mode 100644 index 00000000..c10dd36c --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/RoleController.java @@ -0,0 +1,137 @@ +package sureness.controller; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import sureness.pojo.dto.Message; +import sureness.pojo.entity.AuthResourceDO; +import sureness.pojo.entity.AuthRoleDO; +import sureness.service.RoleService; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 00:24 2019-08-01 + */ +@RestController +@RequestMapping("/role") +@Slf4j +public class RoleController { + + @Autowired + private RoleService roleService; + + + @GetMapping("/resource/{roleId}/{currentPage}/{pageSize}") + public ResponseEntity getResourceOwnByRole(@PathVariable @NotBlank Long roleId, @PathVariable Integer currentPage, @PathVariable Integer pageSize) { + if (currentPage == null){ + currentPage = 1; + } + if (pageSize == null) { + pageSize = 10; + } + Page resourcePage = roleService.getPageResourceOwnRole(roleId, currentPage, pageSize); + Message message = Message.builder().data(resourcePage).build(); + return ResponseEntity.ok().body(message); + } + + @GetMapping("/resource/-/{roleId}/{currentPage}/{pageSize}") + public ResponseEntity getResourceNotOwnByRole(@PathVariable @NotBlank Long roleId, @PathVariable Integer currentPage, @PathVariable Integer pageSize) { + if (currentPage == null){ + currentPage = 1; + } + if (pageSize == null) { + pageSize = 10; + } + Page resourcePage = roleService.getPageResourceNotOwnRole(roleId, currentPage, pageSize); + Message message = Message.builder().data(resourcePage).build(); + return ResponseEntity.ok().body(message); + } + + @PostMapping("/authority/resource/{roleId}/{resourceId}") + public ResponseEntity authorityRoleResource(@PathVariable @NotBlank Long roleId, + @PathVariable @NotBlank Long resourceId) { + roleService.authorityRoleResource(roleId,resourceId); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @DeleteMapping("/authority/resource/{roleId}/{resourceId}") + public ResponseEntity deleteAuthorityRoleResource(@PathVariable @NotBlank Long roleId, + @PathVariable @NotBlank Long resourceId) { + roleService.deleteAuthorityRoleResource(roleId,resourceId); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @PostMapping + public ResponseEntity addRole(@RequestBody @Validated AuthRoleDO authRole) { + if (roleService.addRole(authRole)) { + if (log.isDebugEnabled()) { + log.debug("add role success: {}", authRole); + } + return ResponseEntity.status(HttpStatus.CREATED).build(); + } else { + Message message = Message.builder() + .errorMsg("role already exist").build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + } + + @PutMapping + public ResponseEntity updateRole(@RequestBody @Validated AuthRoleDO authRole) { + if (roleService.updateRole(authRole)) { + if (log.isDebugEnabled()) { + log.debug("update role success: {}", authRole); + } + return ResponseEntity.ok().build(); + } else { + Message message = Message.builder() + .errorMsg("role not exist").build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + } + + @DeleteMapping("/{roleId}") + public ResponseEntity deleteRole(@PathVariable @NotBlank Long roleId) { + if (roleService.deleteRole(roleId)) { + if (log.isDebugEnabled()) { + log.debug("delete role success: {}", roleId); + } + return ResponseEntity.ok().build(); + } else { + Message message = Message.builder() + .errorMsg("delete role fail, no this role here").build(); + log.debug("delete role fail: {}", roleId); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + } + + @GetMapping("/{currentPage}/{pageSize}") + public ResponseEntity getRole(@PathVariable Integer currentPage, @PathVariable Integer pageSize ) { + if (Objects.isNull(currentPage) || Objects.isNull(pageSize)) { + // no pageable + Optional> roleListOptional = roleService.getAllRole(); + if (roleListOptional.isPresent()) { + Message message = Message.builder().data(roleListOptional.get()).build(); + return ResponseEntity.ok().body(message); + } else { + Message message = Message.builder().data(new ArrayList<>()).build(); + return ResponseEntity.ok().body(message); + } + } else { + // pageable + Page rolePage = roleService.getPageRole(currentPage, pageSize); + Message message = Message.builder().data(rolePage).build(); + return ResponseEntity.ok().body(message); + } + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/controller/TokenStorage.java b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/TokenStorage.java new file mode 100644 index 00000000..16e678d0 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/TokenStorage.java @@ -0,0 +1,61 @@ +package sureness.controller; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * token storage + * you can use redis instead of it + * @author tomsun28 + * @date 2020-12-03 23:01 + */ +public class TokenStorage { + + private static final String TOKEN_SPLIT = "--"; + private static final int TOKEN_SPLIT_SIZE = 4; + private static final int START_TIME_INDEX = 1; + private static final int PERIOD_TIME_INDEX = 2; + + private static final Map TOKEN_MAP = new ConcurrentHashMap<>(); + + /** + * match token + * @param key key + * @param currentToken tokenValue is : admin--issueTime--refreshPeriodTime--uuid + * @return false when token not exist, not equals or Expired, else true + */ + public static boolean matchToken(String key, String currentToken) { + if (key == null || currentToken == null || "".equals(key) || "".equals(currentToken) + || currentToken.split(TOKEN_SPLIT).length != TOKEN_SPLIT_SIZE) { + return false; + } + String originToken = TOKEN_MAP.get(key); + if (originToken == null || !originToken.equals(currentToken)) { + removeToken(key); + return false; + } + String[] tokenArr = currentToken.split(TOKEN_SPLIT); + if (Long.parseLong(tokenArr[START_TIME_INDEX]) + Long.parseLong(tokenArr[PERIOD_TIME_INDEX]) + <= System.currentTimeMillis()) { + // token expired, remove it + removeToken(key); + return false; + } + return true; + } + + public static void removeToken(String key) { + if (key == null || "".equals(key)) { + return; + } + TOKEN_MAP.remove(key); + } + + public static void addToken(String key, String token) { + if (key == null || token == null || "".equals(key) || "".equals(token) + || token.split(TOKEN_SPLIT).length != TOKEN_SPLIT_SIZE) { + return; + } + TOKEN_MAP.put(key, token); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/controller/UserController.java b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/UserController.java new file mode 100644 index 00000000..7eb3e451 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/controller/UserController.java @@ -0,0 +1,72 @@ +package sureness.controller; + + +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import sureness.pojo.dto.Message; +import sureness.service.AccountService; + +import java.util.List; + +/** + * + * @author tomsun28 + * @date 21:05 2018/3/17 + */ +@RestController +@RequestMapping("/user") +public class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + @Autowired + private AccountService accountService; + + + @GetMapping("/role") + public ResponseEntity getUserRoles() { + SubjectSum subject = SurenessContextHolder.getBindSubject(); + if (subject == null || subject.getPrincipal() == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + String appId = (String) subject.getPrincipal(); + List roles = accountService.loadAccountRoles(appId); + return ResponseEntity.ok(Message.builder().data(roles).build()); + } + + @PostMapping("/authority/role/{appId}/{roleId}") + public ResponseEntity authorityUserRole(@PathVariable String appId, @PathVariable Long roleId) { + SubjectSum subject = SurenessContextHolder.getBindSubject(); + if (subject == null || subject.getPrincipal() == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + String principal = (String) subject.getPrincipal(); + if (!principal.equals(appId)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + boolean flag = accountService.authorityUserRole(appId, roleId); + return flag ? ResponseEntity.ok().build() : ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + @DeleteMapping("/authority/role/{appId}/{roleId}") + public ResponseEntity deleteAuthorityUserRole(@PathVariable String appId, @PathVariable Long roleId) { + SubjectSum subject = SurenessContextHolder.getBindSubject(); + if (subject == null || subject.getPrincipal() == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + String principal = (String) subject.getPrincipal(); + if (!principal.equals(appId)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + return accountService.deleteAuthorityUserRole(appId, roleId) ? + ResponseEntity.ok().build() : ResponseEntity.status(HttpStatus.CONFLICT).build(); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthResourceDao.java b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthResourceDao.java new file mode 100644 index 00000000..2fd04581 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthResourceDao.java @@ -0,0 +1,64 @@ +package sureness.dao; + +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 sureness.pojo.entity.AuthResourceDO; + +import java.util.List; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 16:40 2019-07-27 + */ +public interface AuthResourceDao extends JpaRepository { + + /** + * Get uri resource and resource-role relationship chain, eg: /api/v2/host===post===[role2,role3,role4] + * @return resource-role chain set + */ + @Query(value = "SELECT CONCAT(LOWER(res.uri),\"===\",LOWER(res.method),\"===[\",IFNULL(GROUP_CONCAT(DISTINCT role.code),\"\"),\"]\") " + + "FROM auth_resource res " + + "LEFT JOIN auth_role_resource_bind bind on res.id = bind.resource_id " + + "LEFT JOIN auth_role role on role.id = bind.role_id " + + "where res.status = 1 " + + "group by res.id", nativeQuery = true) + Optional> getEnableResourcePathRoleData(); + + + + /** + * Get disabled uri resources eg: /api/v2/host===post + * @return resouce set + */ + @Query("select CONCAT(LOWER(resource.uri),'===', resource.method) " + + "from AuthResourceDO resource where resource.status = 9 order by resource.id") + Optional> getDisableResourcePathData(); + + /** + * Get the available API resources owned by the current role in the form of paging + * @param roleId roleId + * @param request page + * @return api resource list + */ + @Query("select distinct resource from AuthResourceDO resource " + + "left join AuthRoleResourceBindDO bind on bind.resourceId = resource.id " + + "where bind.roleId = :roleId " + + "order by resource.id asc") + Page findRoleOwnResource(@Param("roleId") Long roleId, Pageable request); + + /** + * Get the available API resources owned by the current role in the form of paging + * @param roleId roleId + * @param request page + * @return api resource list + */ + @Query("select distinct resource from AuthResourceDO resource " + + " where resource.id not in " + + "(select distinct bind.resourceId from AuthRoleResourceBindDO bind where bind.roleId = :roleId) " + + "order by resource.id asc ") + Page findRoleNotOwnResource(@Param("roleId") Long roleId, Pageable request); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthRoleDao.java b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthRoleDao.java new file mode 100644 index 00000000..08a05946 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthRoleDao.java @@ -0,0 +1,24 @@ +package sureness.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import sureness.pojo.entity.AuthRoleDO; + +import java.util.List; + +/** + * @author tomsun28 + * @date 16:42 2019-07-27 + */ +public interface AuthRoleDao extends JpaRepository { + + /** + * Query the role owned by the current user + * @param username username + * @return role list + */ + @Query("select ar.name from AuthRoleDO ar, AuthUserDO au, AuthUserRoleBindDO bind " + + "where ar.id = bind.roleId and au.id = bind.userId and au.username = :username") + List findAccountOwnRoles(@Param("username") String username); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthRoleResourceBindDao.java b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthRoleResourceBindDao.java new file mode 100644 index 00000000..ef7a5625 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthRoleResourceBindDao.java @@ -0,0 +1,35 @@ +package sureness.dao; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import sureness.pojo.entity.AuthResourceDO; +import sureness.pojo.entity.AuthRoleResourceBindDO; + +import java.util.List; + +/** + * @author tomsun28 + * @date 16:43 2019-07-27 + */ +public interface AuthRoleResourceBindDao extends JpaRepository { + + /** + * Query the resources owned by the current role + * @param roleId roleId + * @return resource list + */ + @Query("select rs from AuthResourceDO rs, AuthRoleResourceBindDO bind " + + "where rs.id = bind.resourceId and bind.roleId = :roleId") + List findRoleBindResourceList(@Param("roleId") Long roleId); + + /** + * delete record which roleId and resource equals this + * @param roleId roleID + * @param resourceId resourceId + */ + @Query("delete from AuthRoleResourceBindDO bind " + + "where bind.roleId = :roleId and bind.resourceId = :resourceId") + void deleteRoleResourceBind(@Param("roleId") Long roleId,@Param("resourceId") Long resourceId); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthUserDao.java b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthUserDao.java new file mode 100644 index 00000000..e44149b1 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthUserDao.java @@ -0,0 +1,34 @@ +package sureness.dao; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import sureness.pojo.entity.AuthUserDO; + +import java.util.List; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 16:43 2019-07-27 + */ +public interface AuthUserDao extends JpaRepository { + + /** + * Get user by username + * @param username username + * @return user + */ + @Query("select au from AuthUserDO au where au.username = :username") + Optional findAuthUserByUsername(@Param("username") String username); + + /** + * Query the role owned by the current user + * @param username username + * @return role list + */ + @Query("select ar.code from AuthRoleDO ar, AuthUserDO au, AuthUserRoleBindDO bind " + + "where ar.id = bind.roleId and au.id = bind.userId and au.username = :username") + List findAccountOwnRoles(@Param("username") String username); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthUserRoleBindDao.java b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthUserRoleBindDao.java new file mode 100644 index 00000000..f2e78dc6 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/dao/AuthUserRoleBindDao.java @@ -0,0 +1,35 @@ +package sureness.dao; + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import sureness.pojo.entity.AuthRoleDO; +import sureness.pojo.entity.AuthUserRoleBindDO; + +import java.util.List; + +/** + * @author tomsun28 + * @date 16:44 2019-07-27 + */ +public interface AuthUserRoleBindDao extends JpaRepository { + + /** + * Query the role owned by the current user + * @param userId userId + * @return role list + */ + @Query("select ar from AuthRoleDO ar, AuthUserRoleBindDO bind " + + "where ar.id = bind.roleId and bind.userId = :userId") + List findUserBindRoleList(@Param("userId") Long userId); + + /** + * delete record which roleId and userId equals this + * @param roleId roleID + * @param userId userId + */ + @Query("delete from AuthUserRoleBindDO bind " + + "where bind.roleId = :roleId and bind.userId = :userId") + void deleteRoleResourceBind(@Param("roleId") Long roleId,@Param("userId") Long userId); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/handler/GlobalExceptionHandler.java b/samples/shenyu-sureness-sql/src/main/java/sureness/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..3d80f5d8 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/handler/GlobalExceptionHandler.java @@ -0,0 +1,95 @@ +package sureness.handler; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import sureness.pojo.dto.Message; +import sureness.service.impl.DataConflictException; + +/** + * controller exception handler + * @author tomsun28 + * @date 22:45 2019-08-01 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * handler the exception thrown for data input verify + * @param exception data input verify exception + * @return response + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseBody + ResponseEntity handleInputValidException(MethodArgumentNotValidException exception) { + StringBuffer errorMessage = new StringBuffer(); + if (exception != null) { + exception.getBindingResult().getAllErrors().forEach(error -> + errorMessage.append(error.getDefaultMessage()).append(".")); + } + if (log.isDebugEnabled()) { + log.debug("[sample-tom]-[input argument not valid happen]-{}", errorMessage, exception); + } + Message message = Message.builder().errorMsg(errorMessage.toString()).build(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); + } + + /** + * handler the exception thrown for datastore error + * @param exception datastore exception + * @return response + */ + @ExceptionHandler(DataAccessException.class) + @ResponseBody + ResponseEntity handleDataAccessException(DataAccessException exception) { + String errorMessage = "database error happen"; + if (exception != null) { + errorMessage = exception.getMessage(); + } + log.warn("[sample-tom]-[database error happen]-{}", errorMessage, exception); + Message message = Message.builder().errorMsg(errorMessage).build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + + /** + * handler the exception thrown for data conflict + * @param exception data conflict + * @return response + */ + @ExceptionHandler(DataConflictException.class) + @ResponseBody + ResponseEntity handleDataConflictException(DataConflictException exception) { + String errorMessage = "data status conflict warning"; + if (exception != null && exception.getMessage() != null) { + errorMessage = exception.getMessage(); + } + log.info("[sample-tom]-[data status conflict warning]-{}", errorMessage, exception); + Message message = Message.builder().errorMsg(errorMessage).build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + + /** + * handler the exception thrown for unCatch and unKnown + * @param exception UnknownException + * @return response + */ + @ExceptionHandler(Exception.class) + @ResponseBody + ResponseEntity handleUnknownException(Exception exception) { + String errorMessage = "unknown error happen"; + if (exception != null) { + errorMessage = exception.getMessage(); + } + log.error("[sample-tom]-[unknown error happen]-{}", errorMessage, exception); + Message message = Message.builder().errorMsg(errorMessage).build(); + return ResponseEntity.status(HttpStatus.CONFLICT).body(message); + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/dto/Account.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/dto/Account.java new file mode 100644 index 00000000..9195039a --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/dto/Account.java @@ -0,0 +1,29 @@ +package sureness.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; + +/** + * @author tomsun28 + * @date 20:36 2019-08-01 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Account { + + @NotBlank(message = "username can not null") + @Length(min = 3, max = 100, message = "username length in 3-100") + private String username; + + @NotBlank(message = "password can not null") + @Length(min = 3, max = 100, message = "password length in 3-100") + private String password; + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/dto/Message.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/dto/Message.java new file mode 100644 index 00000000..32812d97 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/dto/Message.java @@ -0,0 +1,34 @@ +package sureness.pojo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Unified message structure definition for front and back ends + * + * { + * data:{....}, + * errorMsg: message + * } + * @author tomsun28 + * @date 23:48 2019/08/01 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Message { + + /** + * message body data + */ + private Object data; + + /** + * exception message when error happen + */ + private String errorMsg; + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthResourceDO.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthResourceDO.java new file mode 100644 index 00000000..efd50465 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthResourceDO.java @@ -0,0 +1,54 @@ +package sureness.pojo.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.Range; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.time.LocalDateTime; + +/** + * resource entity + * @author tomsun28 + * @date 00:00 2019-07-26 + */ +@Entity +@Table(name = "auth_resource") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthResourceDO { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "name can not null") + @Length(min = 3, max = 100, message = "name length in 3-100") + private String name; + + @NotBlank(message = "code can not null") + @Length(min = 3, max = 100, message = "code length in 3-100") + private String code; + + @NotBlank(message = "uri can not null") + private String uri; + + private String type; + + @NotBlank(message = "method can not null") + private String method; + + @Range(min = 0, max = 9, message = "1 enable, 9 disable") + private Integer status; + + private String description; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthRoleDO.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthRoleDO.java new file mode 100644 index 00000000..c72c0136 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthRoleDO.java @@ -0,0 +1,47 @@ +package sureness.pojo.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.Range; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.time.LocalDateTime; + +/** + * role entity + * @author tomsun28 + * @date 00:27 2019-07-27 + */ +@Entity +@Table(name = "auth_role") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthRoleDO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "name can not null") + @Length(min = 3, max = 100, message = "name length in 3-100") + private String name; + + @NotBlank(message = "code can not null") + @Length(min = 3, max = 100, message = "code length in 3-100") + private String code; + + @Range(min = 0, max = 9, message = "1 enable, 9 disable") + private Integer status; + + private String description; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthRoleResourceBindDO.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthRoleResourceBindDO.java new file mode 100644 index 00000000..b8fdfb3a --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthRoleResourceBindDO.java @@ -0,0 +1,38 @@ +package sureness.pojo.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime;; + +/** + * resource-role mapping entity + * @author tomsun28 + * @date 00:28 2019-07-27 + */ +@Entity +@Table(name = "auth_role_resource_bind") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthRoleResourceBindDO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull(message = "roleId can not null") + private Long roleId; + + @NotNull(message = "resourceId can not null") + private Long resourceId; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthUserDO.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthUserDO.java new file mode 100644 index 00000000..e8896eb5 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthUserDO.java @@ -0,0 +1,58 @@ +package sureness.pojo.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.Range; + + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.time.LocalDateTime; + +/** + * user entity + * @author tomsun28 + * @date 00:29 2019-07-27 + */ +@Entity +@Table(name = "auth_user") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthUserDO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "username can not null") + @Length(min = 3, max = 100, message = "name length in 3-100") + private String username; + + @NotBlank(message = "password can not null") + @Length(min = 3, max = 100, message = "password length in 3-100") + private String password; + + private String salt; + + private String avatar; + + private String phone; + + private String email; + + private Integer sex; + + @Range(min = 0, max = 9, message = "1 enable, 2 locked, 3 deleted, 4 illegal") + private Integer status; + + private Integer createWhere; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthUserRoleBindDO.java b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthUserRoleBindDO.java new file mode 100644 index 00000000..5386d8f2 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/pojo/entity/AuthUserRoleBindDO.java @@ -0,0 +1,38 @@ +package sureness.pojo.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * user-role mapping entity + * @author tomsun28 + * @date 00:30 2019-07-27 + */ +@Entity +@Table(name = "auth_user_role_bind") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthUserRoleBindDO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull(message = "userId can not null") + private Long userId; + + @NotNull(message = "roleId can not null") + private Long roleId; + + private LocalDateTime gmtCreate; + + private LocalDateTime gmtUpdate; +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/AccountService.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/AccountService.java new file mode 100644 index 00000000..e4b06a2f --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/AccountService.java @@ -0,0 +1,63 @@ +package sureness.service; + +import com.usthe.sureness.provider.SurenessAccount; +import sureness.pojo.dto.Account; + +import java.util.List; + +/** + * @author tomsun28 + * @date 00:12 2019-08-01 + */ +public interface AccountService { + /** + * Verify account validity, username and password + * @param account account info + * @return success-true failed-false + */ + boolean authenticateAccount(Account account); + + /** + * Get all roles owned by this username account, combine them into string list + * @param username account username + * @return role-string eg role1,role3,role2 + */ + List loadAccountRoles(String username); + + /** + * register account + * @param account account info + * @return success-true failed-false + */ + boolean registerAccount(Account account); + + /** + * Determine whether the account already exists + * @param account account info + * @return exist-true no-false + */ + boolean isAccountExist(Account account); + + /** + * Load the account information by username + * @param username account username + * @return account + */ + SurenessAccount loadAccount(String username); + + /** + * authority User Role by username and roleId + * @param appId account username + * @param roleId roleId + * @return success-true failed-false + */ + boolean authorityUserRole(String appId, Long roleId); + + /** + * delete authority User Role by username and roleId + * @param appId account username + * @param roleId roleId + * @return success-true failed-false + */ + boolean deleteAuthorityUserRole(String appId, Long roleId); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/ResourceService.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/ResourceService.java new file mode 100644 index 00000000..dd34cc60 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/ResourceService.java @@ -0,0 +1,70 @@ +package sureness.service; + + +import org.springframework.data.domain.Page; +import sureness.pojo.entity.AuthResourceDO; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * @author tomsun28 + * @date 00:13 2019-08-01 + */ +public interface ResourceService { + + /** + * add uri resource + * @param authResource resource + * @return success-true failed-false + */ + boolean addResource(AuthResourceDO authResource); + + /** + * Determine whether the resource already exists + * @param authResource resource + * @return existed-true no-false + */ + boolean isResourceExist(AuthResourceDO authResource); + + /** + * update uri resource + * @param authResource resource + * @return success-true failed-false + */ + boolean updateResource(AuthResourceDO authResource); + + /** + * delete uri resource + * @param resourceId resource ID + * @return success-true no existed-false + */ + boolean deleteResource(Long resourceId); + + /** + * get all resources + * @return resource list + */ + Optional> getAllResource(); + + /** + * get resource by page + * @param currentPage current page + * @param pageSize page size + * @return Page of resource + */ + Page getPageResource(Integer currentPage, Integer pageSize); + + /** + * get enabled resource-path-role eg: /api/v2/host===post===[role2,role3,role4] + * @return resource-path-role + */ + Set getAllEnableResourcePath(); + + /** + * get disable resource-path-role eg: /api/v2/host===post===[role2,role3,role4] + * @return resource-path-role + */ + Set getAllDisableResourcePath(); +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/RoleService.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/RoleService.java new file mode 100644 index 00000000..85b155ad --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/RoleService.java @@ -0,0 +1,91 @@ +package sureness.service; + + +import org.springframework.data.domain.Page; +import sureness.pojo.entity.AuthResourceDO; +import sureness.pojo.entity.AuthRoleDO; + +import java.util.List; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 00:14 2019-08-01 + */ +public interface RoleService { + + /** + * Determine whether the role already exists + * @param authRole role + * @return existed-true no-false + */ + boolean isRoleExist(AuthRoleDO authRole); + + /** + * add role + * @param authRole role + * @return add success-true failed-false + */ + boolean addRole(AuthRoleDO authRole); + + /** + * update role + * @param authRole role + * @return success-true failed-false + */ + boolean updateRole(AuthRoleDO authRole); + + /** + * delete role + * @param roleId role ID + * @return success-true failed-false + */ + boolean deleteRole(Long roleId); + + /** + * get all role list + * @return role list + */ + Optional> getAllRole(); + + /** + * get roles page + * @param currentPage current page + * @param pageSize page size + * @return Page of roles + */ + Page getPageRole(Integer currentPage, Integer pageSize); + + /** + * get pageable resources which this role owned + * @param roleId role ID + * @param currentPage current page + * @param pageSize page size + * @return Page of resources + */ + Page getPageResourceOwnRole(Long roleId, Integer currentPage, Integer pageSize); + + /** + * get pageable resources which this role not owned + * @param roleId role ID + * @param currentPage current page + * @param pageSize page size + * @return Page of resources + */ + Page getPageResourceNotOwnRole(Long roleId, Integer currentPage, Integer pageSize); + + /** + * authority this resource to this role + * @param roleId role ID + * @param resourceId resource ID + */ + void authorityRoleResource(Long roleId, Long resourceId); + + /** + * unAuthority this resource in this role + * @param roleId role ID + * @param resourceId resource ID + */ + void deleteAuthorityRoleResource(Long roleId, Long resourceId); + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/AccountServiceImpl.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/AccountServiceImpl.java new file mode 100644 index 00000000..7aaf4bf2 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/AccountServiceImpl.java @@ -0,0 +1,121 @@ +package sureness.service.impl; + +import com.usthe.sureness.provider.DefaultAccount; +import com.usthe.sureness.provider.SurenessAccount; +import com.usthe.sureness.util.Md5Util; +import com.usthe.sureness.util.SurenessCommonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sureness.dao.AuthUserDao; +import sureness.dao.AuthUserRoleBindDao; +import sureness.pojo.dto.Account; +import sureness.pojo.entity.AuthUserDO; +import sureness.pojo.entity.AuthUserRoleBindDO; +import sureness.service.AccountService; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 10:58 2019-08-04 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class AccountServiceImpl implements AccountService { + + @Autowired + private AuthUserDao authUserDao; + + private AuthUserRoleBindDao userRoleBindDao; + + @Override + public boolean authenticateAccount(Account account) { + Optional authUserOptional = authUserDao.findAuthUserByUsername(account.getUsername()); + if (!authUserOptional.isPresent()) { + return false; + } + AuthUserDO authUser = authUserOptional.get(); + String password = account.getPassword(); + if (password == null) { + return false; + } + if (Objects.nonNull(authUser.getSalt())) { + // md5 with salt + password = Md5Util.md5(password + authUser.getSalt()); + + } + return authUser.getPassword().equals(password); + } + + @Override + public List loadAccountRoles(String username) { + return authUserDao.findAccountOwnRoles(username); + } + + @Override + public boolean registerAccount(Account account) { + if (isAccountExist(account)) { + return false; + } + String salt = SurenessCommonUtil.getRandomString(6); + String password = Md5Util.md5(account.getPassword() + salt); + AuthUserDO authUser = AuthUserDO.builder().username(account.getUsername()) + .password(password).salt(salt).status(1).build(); + authUserDao.save(authUser); + return true; + } + + @Override + public boolean isAccountExist(Account account) { + Optional authUserOptional = authUserDao.findAuthUserByUsername(account.getUsername()); + return authUserOptional.isPresent(); + } + + @Override + public SurenessAccount loadAccount(String username) { + Optional authUserOptional = authUserDao.findAuthUserByUsername(username); + if (authUserOptional.isPresent()) { + AuthUserDO authUser = authUserOptional.get(); + DefaultAccount.Builder accountBuilder = DefaultAccount.builder(username) + .setPassword(authUser.getPassword()) + .setSalt(authUser.getSalt()) + .setDisabledAccount(1 != authUser.getStatus()) + .setExcessiveAttempts(2 == authUser.getStatus()); + List roles = loadAccountRoles(username); + if (roles != null) { + accountBuilder.setOwnRoles(roles); + } + return accountBuilder.build(); + } else { + return null; + } + } + + @Override + public boolean authorityUserRole(String appId, Long roleId) { + Optional optional = authUserDao.findAuthUserByUsername(appId); + if (!optional.isPresent()) { + return false; + } + Long userId = optional.get().getId(); + AuthUserRoleBindDO userRoleBindDO = AuthUserRoleBindDO.builder().userId(userId).roleId(roleId).build(); + + userRoleBindDao.save(userRoleBindDO); + return true; + } + + @Override + public boolean deleteAuthorityUserRole(String appId, Long roleId) { + Optional optional = authUserDao.findAuthUserByUsername(appId); + if (!optional.isPresent()) { + return false; + } + Long userId = optional.get().getId(); + userRoleBindDao.deleteRoleResourceBind(roleId, userId); + return true; + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/DataConflictException.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/DataConflictException.java new file mode 100644 index 00000000..df04a8f5 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/DataConflictException.java @@ -0,0 +1,14 @@ +package sureness.service.impl; + + +/** + * data conflict exception + * @author tomsun28 + * @date 22:55 2020-04-27 + */ +public class DataConflictException extends RuntimeException { + + public DataConflictException(String msg) { + super(msg); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/ResourceServiceImpl.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/ResourceServiceImpl.java new file mode 100644 index 00000000..46e283b4 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/ResourceServiceImpl.java @@ -0,0 +1,93 @@ +package sureness.service.impl; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sureness.dao.AuthResourceDao; +import sureness.pojo.entity.AuthResourceDO; +import sureness.service.ResourceService; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * @author tomsun28 + * @date 13:09 2019-08-04 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class ResourceServiceImpl implements ResourceService { + + @Autowired + private AuthResourceDao authResourceDao; + + @Override + public boolean addResource(AuthResourceDO authResource) { + if (isResourceExist(authResource)) { + return false; + } else { + authResourceDao.saveAndFlush(authResource); + return true; + } + } + + @Override + public boolean isResourceExist(AuthResourceDO authResource) { + AuthResourceDO resource = AuthResourceDO.builder() + .uri(authResource.getUri()) + .method(authResource.getMethod()) + .build(); + Example example = Example.of(resource); + return authResourceDao.exists(example); + } + + @Override + public boolean updateResource(AuthResourceDO authResource) { + if (authResourceDao.existsById(authResource.getId())) { + authResourceDao.saveAndFlush(authResource); + return true; + } else { + return false; + } + } + + @Override + public boolean deleteResource(Long resourceId) { + if (authResourceDao.existsById(resourceId)) { + authResourceDao.deleteById(resourceId); + return true; + } else { + return false; + } + } + + @Override + public Optional> getAllResource() { + List resourceList = authResourceDao.findAll(); + return Optional.of(resourceList); + } + + @Override + public Page getPageResource(Integer currentPage, Integer pageSize) { + PageRequest pageRequest = PageRequest.of(currentPage, pageSize); + return authResourceDao.findAll(pageRequest); + } + + @Override + public Set getAllEnableResourcePath() { + Optional> optional = authResourceDao.getEnableResourcePathRoleData(); + return optional.>map(HashSet::new).orElseGet(() -> new HashSet<>(0)); + } + + @Override + public Set getAllDisableResourcePath() { + Optional> optional = authResourceDao.getDisableResourcePathData(); + return optional.>map(HashSet::new).orElseGet(() -> new HashSet<>(0)); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/RoleServiceImpl.java b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/RoleServiceImpl.java new file mode 100644 index 00000000..1de7c1eb --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/service/impl/RoleServiceImpl.java @@ -0,0 +1,123 @@ +package sureness.service.impl; + +import com.usthe.sureness.matcher.TreePathRoleMatcher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sureness.dao.AuthResourceDao; +import sureness.dao.AuthRoleDao; +import sureness.dao.AuthRoleResourceBindDao; +import sureness.pojo.entity.AuthResourceDO; +import sureness.pojo.entity.AuthRoleDO; +import sureness.pojo.entity.AuthRoleResourceBindDO; +import sureness.service.RoleService; + +import java.util.List; +import java.util.Optional; + +/** + * @author tomsun28 + * @date 13:10 2019-08-04 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class RoleServiceImpl implements RoleService { + + @Autowired + private AuthRoleDao authRoleDao; + + @Autowired + private AuthResourceDao authResourceDao; + + @Autowired + private AuthRoleResourceBindDao roleResourceBindDao; + + @Autowired + private TreePathRoleMatcher treePathRoleMatcher; + + @Override + public boolean isRoleExist(AuthRoleDO authRole) { + AuthRoleDO role = AuthRoleDO.builder() + .name(authRole.getName()).code(authRole.getCode()).build(); + return authRoleDao.exists(Example.of(role)); + } + + @Override + public boolean addRole(AuthRoleDO authRole) { + if (isRoleExist(authRole)) { + return false; + } else { + authRoleDao.saveAndFlush(authRole); + return true; + } + } + + @Override + public boolean updateRole(AuthRoleDO authRole) { + if (authRoleDao.existsById(authRole.getId())) { + authRoleDao.saveAndFlush(authRole); + return true; + } else { + return false; + } + } + + @Override + public boolean deleteRole(Long roleId) { + if (authRoleDao.existsById(roleId)) { + authRoleDao.deleteById(roleId); + return true; + } else { + return false; + } + } + + @Override + public Optional> getAllRole() { + List roleList = authRoleDao.findAll(); + return Optional.of(roleList); + } + + @Override + public Page getPageRole(Integer currentPage, Integer pageSize) { + PageRequest pageRequest = PageRequest.of(currentPage, pageSize); + return authRoleDao.findAll(pageRequest); + } + + @Override + public Page getPageResourceOwnRole(Long roleId, Integer currentPage, Integer pageSize) { + PageRequest pageRequest = PageRequest.of(currentPage, pageSize, Sort.Direction.ASC, "id"); + return authResourceDao.findRoleOwnResource(roleId, pageRequest); + } + + @Override + public Page getPageResourceNotOwnRole(Long roleId, Integer currentPage, Integer pageSize) { + PageRequest pageRequest = PageRequest.of(currentPage, pageSize, Sort.Direction.ASC, "id"); + return authResourceDao.findRoleNotOwnResource(roleId, pageRequest); + } + + @Override + public void authorityRoleResource(Long roleId, Long resourceId) { + // Determine whether this resource and role exist + if (!authRoleDao.existsById(roleId) || !authResourceDao.existsById(resourceId)) { + throw new DataConflictException("roleId or resourceId not exist"); + } + // insert it in database, if existed the unique index will work + AuthRoleResourceBindDO bind = AuthRoleResourceBindDO + .builder().roleId(roleId).resourceId(resourceId).build(); + roleResourceBindDao.saveAndFlush(bind); + // refresh resource path data tree + treePathRoleMatcher.rebuildTree(); + } + + @Override + public void deleteAuthorityRoleResource(Long roleId, Long resourceId) { + roleResourceBindDao.deleteRoleResourceBind(roleId, resourceId); + // refresh resource path data tree + treePathRoleMatcher.rebuildTree(); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/support/BasicSubjectReactiveCreator.java b/samples/shenyu-sureness-sql/src/main/java/sureness/support/BasicSubjectReactiveCreator.java new file mode 100644 index 00000000..85277f4d --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/support/BasicSubjectReactiveCreator.java @@ -0,0 +1,74 @@ +package sureness.support; + +import com.usthe.sureness.subject.Subject; +import com.usthe.sureness.subject.SubjectCreate; +import com.usthe.sureness.subject.support.PasswordSubject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * the creator to create PasswordSubject(basic auth) + * only support reactive server-side HTTP request + * org.springframework.http.server.reactive.ServerHttpRequest + * @author tomsun28 + * @date 23:53 2020-02-27 + */ +public class BasicSubjectReactiveCreator implements SubjectCreate { + + private static final Logger logger = LoggerFactory.getLogger(BasicSubjectReactiveCreator.class); + + private static final String AUTHORIZATION = "Authorization"; + private static final String BASIC = "Basic"; + private static final int COUNT_2 = 2; + + @Override + public boolean canSupportSubject(Object context) { + // ("Authorization", "Basic YWRtaW46YWRtaW4=") --- basic auth + if (context instanceof ServerHttpRequest) { + String authorization = ((ServerHttpRequest)context).getHeaders().getFirst(AUTHORIZATION); + return authorization != null && authorization.startsWith(BASIC); + } else { + return false; + } + } + + @Override + public Subject createSubject(Object context) { + String authorization = ((ServerHttpRequest)context).getHeaders().getFirst(AUTHORIZATION); + if (authorization == null) { + return null; + } + //basic auth + String basicAuth = authorization.replace(BASIC, "").trim(); + basicAuth = new String(Base64.getDecoder().decode(basicAuth), StandardCharsets.UTF_8); + String[] auth = basicAuth.split(":"); + if (auth.length != COUNT_2) { + if (logger.isInfoEnabled()) { + logger.info("can not create basic auth PasswordSubject by this request message"); + } + return null; + } + String username = auth[0]; + if (username == null || "".equals(username)) { + if (logger.isInfoEnabled()) { + logger.info("can not create basic auth PasswordSubject by this request message, appId can not null"); + } + return null; + } + String password = auth[1]; + InetSocketAddress remoteAddress = ((ServerHttpRequest) context).getRemoteAddress(); + String remoteHost = remoteAddress == null ? "" : remoteAddress.getHostString(); + String requestUri = ((ServerHttpRequest) context).getPath().value(); + String requestType = ((ServerHttpRequest) context).getMethodValue(); + String targetUri = requestUri.concat("===").concat(requestType).toLowerCase(); + return PasswordSubject.builder(username, password) + .setRemoteHost(remoteHost) + .setTargetResource(targetUri) + .build(); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/support/JwtSubjectReactiveCreator.java b/samples/shenyu-sureness-sql/src/main/java/sureness/support/JwtSubjectReactiveCreator.java new file mode 100644 index 00000000..19d0e919 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/support/JwtSubjectReactiveCreator.java @@ -0,0 +1,65 @@ +package sureness.support; + +import com.usthe.sureness.subject.Subject; +import com.usthe.sureness.subject.SubjectCreate; +import com.usthe.sureness.subject.support.JwtSubject; +import com.usthe.sureness.util.JsonWebTokenUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.net.InetSocketAddress; + + +/** + * JwtSubject creator + * only support ServerHttpRequest + * @author tomsun28 + * @date 23:58 2020-09-29 + */ +public class JwtSubjectReactiveCreator implements SubjectCreate { + + private static final Logger logger = LoggerFactory.getLogger(JwtSubjectReactiveCreator.class); + + private static final String BEARER = "Bearer"; + private static final String AUTHORIZATION = "Authorization"; + + @Override + public boolean canSupportSubject(Object context) { + // support bearer jwt + // ("Authorization", "Bearer eyJhbGciOiJIUzUxMi...") --- jwt auth + if (context instanceof ServerHttpRequest) { + String authorization = ((ServerHttpRequest)context).getHeaders().getFirst(AUTHORIZATION); + if (authorization != null && authorization.startsWith(BEARER)) { + String jwtValue = authorization.replace(BEARER, "").trim(); + return !JsonWebTokenUtil.isNotJsonWebToken(jwtValue); + } + } + return false; + } + + @Override + public Subject createSubject(Object context) { + String authorization = ((ServerHttpRequest)context).getHeaders().getFirst(AUTHORIZATION); + if (authorization != null && authorization.startsWith(BEARER)) { + // jwt token + String jwtValue = authorization.replace(BEARER, "").trim(); + if (JsonWebTokenUtil.isNotJsonWebToken(jwtValue)) { + if (logger.isInfoEnabled()) { + logger.info("can not create JwtSubject by this request message, is not jwt"); + } + return null; + } + InetSocketAddress remoteAddress = ((ServerHttpRequest) context).getRemoteAddress(); + String remoteHost = remoteAddress == null ? "" : remoteAddress.getHostString(); + String requestUri = ((ServerHttpRequest) context).getPath().value(); + String requestType = ((ServerHttpRequest) context).getMethodValue(); + String targetUri = requestUri.concat("===").concat(requestType.toLowerCase()); + return JwtSubject.builder(jwtValue) + .setRemoteHost(remoteHost) + .setTargetResource(targetUri) + .build(); + } + return null; + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/support/NoneSubjectReactiveCreator.java b/samples/shenyu-sureness-sql/src/main/java/sureness/support/NoneSubjectReactiveCreator.java new file mode 100644 index 00000000..9e3d3c87 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/support/NoneSubjectReactiveCreator.java @@ -0,0 +1,33 @@ +package sureness.support; + +import com.usthe.sureness.subject.Subject; +import com.usthe.sureness.subject.SubjectCreate; +import com.usthe.sureness.subject.support.NoneSubject; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.net.InetSocketAddress; + +/** + * the creator to create NoneSubject - no auth info + * all request can be created a NoneSubject by NoneSubjectReactiveCreator + * only support ServerHttpRequest + * @author tomsun28 + * @date 22:55 2020-09-29 + */ +public class NoneSubjectReactiveCreator implements SubjectCreate { + @Override + public boolean canSupportSubject(Object context) { + return context instanceof ServerHttpRequest; + } + + @Override + public Subject createSubject(Object context) { + InetSocketAddress remoteAddress = ((ServerHttpRequest) context).getRemoteAddress(); + String remoteHost = remoteAddress == null ? "" : remoteAddress.getHostString(); + String requestUri = ((ServerHttpRequest) context).getPath().value(); + String requestType = ((ServerHttpRequest) context).getMethodValue(); + String targetUri = requestUri.concat("===").concat(requestType).toLowerCase(); + return NoneSubject.builder().setRemoteHost(remoteHost) + .setTargetUri(targetUri).build(); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/SurenessFilterExample.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/SurenessFilterExample.java new file mode 100644 index 00000000..1ea3a2d8 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/SurenessFilterExample.java @@ -0,0 +1,105 @@ +package sureness.sureness; + +import com.usthe.sureness.mgt.SurenessSecurityManager; +import com.usthe.sureness.processor.exception.ExpiredCredentialsException; +import com.usthe.sureness.processor.exception.IncorrectCredentialsException; +import com.usthe.sureness.processor.exception.UnauthorizedException; +import com.usthe.sureness.processor.exception.UnknownAccountException; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import java.nio.charset.StandardCharsets; + + +/** + * @title: SurenessConfiguration + * @Author hqgordon + * @Date: 2021/7/31 5:13 下午 + * @Description: sureness过滤器 + * @Version 1.0 + */ +@Configuration +@Order(-1) +public class SurenessFilterExample implements WebFilter { + + private static final Logger logger = LoggerFactory.getLogger(SurenessFilterExample.class); + + /* + * @Author hqgordon + * @Description 过滤方法 + * @Date 2021/8/9 8:29 下午 + * @Param [exchange, chain] + * @return reactor.core.publisher.Mono + **/ + @Override + @Nonnull + public Mono filter(ServerWebExchange exchange,@Nonnull WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + HttpStatus statusCode = null; + String errorMsg = null; + try { + SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(request); + // You can consider using SurenessContextHolder to bind subject in threadLocal + // if bind, please remove it when end + if (subject != null) { + SurenessContextHolder.bindSubject(subject); + } + } catch (IncorrectCredentialsException | UnknownAccountException | ExpiredCredentialsException e1) { + logger.debug("this request account info is illegal"); + statusCode = HttpStatus.UNAUTHORIZED; + errorMsg = e1.getMessage(); + } catch (UnauthorizedException e5) { + logger.debug("this account can not access this resource"); + statusCode = HttpStatus.FORBIDDEN; + errorMsg = e5.getMessage(); + } catch (RuntimeException e) { + logger.error("other exception happen: ", e); + statusCode = HttpStatus.CONFLICT; + errorMsg = e.getMessage(); + } + + // auth error filter to error collect api + if (statusCode != null && errorMsg != null) { + String finalErrorMsg = errorMsg; + Integer finalStatusCode = statusCode.value(); + request = request.mutate().headers(httpHeaders -> { + httpHeaders.add("statusCode", String.valueOf(finalStatusCode)); + httpHeaders.add("errorMsg", finalErrorMsg); + }).path("/auth/error").build(); + exchange = exchange.mutate().request(request).build(); + return responseWrite(exchange,statusCode,errorMsg); + } + return chain.filter(exchange).doFinally(x -> SurenessContextHolder.unbindSubject()); + } + private Mono responseWrite(ServerWebExchange exchange, HttpStatus statusCode, + String message) { + + exchange.getResponse().setStatusCode(statusCode); + if (message != null) { + return exchange.getResponse().writeWith(Flux.create(sink -> { + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false)); + DataBuffer dataBuffer= nettyDataBufferFactory.wrap(message.getBytes(StandardCharsets.UTF_8)); + sink.next(dataBuffer); + sink.complete(); + })); + } else { + return exchange.getResponse().setComplete(); + } + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/config/SurenessConfiguration.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/config/SurenessConfiguration.java new file mode 100644 index 00000000..03261ce5 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/config/SurenessConfiguration.java @@ -0,0 +1,121 @@ +package sureness.sureness.config; + +import com.usthe.sureness.matcher.DefaultPathRoleMatcher; +import com.usthe.sureness.matcher.PathTreeProvider; +import com.usthe.sureness.matcher.TreePathRoleMatcher; +import com.usthe.sureness.mgt.SurenessSecurityManager; +import com.usthe.sureness.processor.DefaultProcessorManager; +import com.usthe.sureness.processor.Processor; +import com.usthe.sureness.processor.ProcessorManager; +import com.usthe.sureness.processor.support.JwtProcessor; +import com.usthe.sureness.processor.support.NoneProcessor; +import com.usthe.sureness.processor.support.PasswordProcessor; +import com.usthe.sureness.provider.SurenessAccountProvider; +import com.usthe.sureness.provider.annotation.AnnotationPathTreeProvider; +import com.usthe.sureness.provider.ducument.DocumentPathTreeProvider; +import com.usthe.sureness.subject.SubjectFactory; +import com.usthe.sureness.subject.SurenessSubjectFactory; +import com.usthe.sureness.util.JsonWebTokenUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import sureness.support.BasicSubjectReactiveCreator; +import sureness.support.JwtSubjectReactiveCreator; +import sureness.support.NoneSubjectReactiveCreator; +import sureness.sureness.processor.CustomTokenProcessor; +import sureness.sureness.subject.CustomPasswdSubjectCreator; +import sureness.sureness.subject.CustomTokenSubjectCreator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * sureness config + * @author tomsun28 + * @date 22:40 2020-03-02 + */ +@Configuration +public class SurenessConfiguration { + + /** + * jwt secret key + */ + private static final String TOM_SECRET_KEY = "?::4s9ssf2sf4sed45pf):" + + "RnLN7XNn4wARoQXizIv6MHUsIV+EFfiMw/x7R0ntu4aWr/CWuApcFaj" + + "CyaFv0bwq2Eik0jdrKUtsA6bx3sDJeFV643R+YYzGMRIqcBIp6AKA98" + + "GM2RIqcBIp6-?::4390fsf4sdl6opf)4ZI:tdQMtcQQ14pkOAQdQ546"; + + @Bean + ProcessorManager processorManager(SurenessAccountProvider accountProvider) { + // process init + List processorList = new LinkedList<>(); + // use default none processor + NoneProcessor noneProcessor = new NoneProcessor(); + processorList.add(noneProcessor); + // use default jwt processor + JwtProcessor jwtProcessor = new JwtProcessor(); + processorList.add(jwtProcessor); + // use default basic auth processor + PasswordProcessor passwordProcessor = new PasswordProcessor(); + passwordProcessor.setAccountProvider(accountProvider); + processorList.add(passwordProcessor); + + // use custom token processor + CustomTokenProcessor customTokenProcessor = new CustomTokenProcessor(); + customTokenProcessor.setAccountProvider(accountProvider); + processorList.add(customTokenProcessor); + return new DefaultProcessorManager(processorList); + } + + /** + * @param databasePathTreeProvider the path tree resource load from database + */ + @Bean + TreePathRoleMatcher pathRoleMatcher(PathTreeProvider databasePathTreeProvider) { + // the path tree resource load from document - sureness.yml + PathTreeProvider documentPathTreeProvider = new DocumentPathTreeProvider(); + // the path tree resource load form annotation - @RequiresRoles @WithoutAuth + AnnotationPathTreeProvider annotationPathTreeProvider = new AnnotationPathTreeProvider(); + annotationPathTreeProvider.setScanPackages(Collections.singletonList("com.usthe.sureness.sample.tom.controller")); + // pathRoleMatcher init + DefaultPathRoleMatcher pathRoleMatcher = new DefaultPathRoleMatcher(); + pathRoleMatcher.setPathTreeProviderList(Arrays.asList( + documentPathTreeProvider, + annotationPathTreeProvider, + databasePathTreeProvider)); + pathRoleMatcher.buildTree(); + return pathRoleMatcher; + } + + @Bean + SubjectFactory subjectFactory() { + // SubjectFactory init + SubjectFactory subjectFactory = new SurenessSubjectFactory(); + subjectFactory.registerSubjectCreator(Arrays.asList( + // attention! must add noSubjectCreator first + new NoneSubjectReactiveCreator(), + // use default basic auth subject creator + new BasicSubjectReactiveCreator(), + // use default jwt subject creator + new JwtSubjectReactiveCreator(), + // use custom password creator + new CustomPasswdSubjectCreator(), + // use custom token creator + new CustomTokenSubjectCreator())); + return subjectFactory; + } + + @Bean + SurenessSecurityManager securityManager(ProcessorManager processorManager, + TreePathRoleMatcher pathRoleMatcher, SubjectFactory subjectFactory) { + JsonWebTokenUtil.setDefaultSecretKey(TOM_SECRET_KEY); + // surenessSecurityManager init + SurenessSecurityManager securityManager = SurenessSecurityManager.getInstance(); + securityManager.setPathRoleMatcher(pathRoleMatcher); + securityManager.setSubjectFactory(subjectFactory); + securityManager.setProcessorManager(processorManager); + return securityManager; + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/processor/CustomTokenProcessor.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/processor/CustomTokenProcessor.java new file mode 100644 index 00000000..c71b75a6 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/processor/CustomTokenProcessor.java @@ -0,0 +1,104 @@ +package sureness.sureness.processor; + +import com.usthe.sureness.processor.BaseProcessor; +import com.usthe.sureness.processor.exception.IncorrectCredentialsException; +import com.usthe.sureness.processor.exception.SurenessAuthenticationException; +import com.usthe.sureness.processor.exception.SurenessAuthorizationException; +import com.usthe.sureness.processor.exception.UnauthorizedException; +import com.usthe.sureness.provider.SurenessAccount; +import com.usthe.sureness.provider.SurenessAccountProvider; +import com.usthe.sureness.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sureness.controller.TokenStorage; +import sureness.sureness.subject.CustomTokenSubject; + +import java.util.List; +import java.util.UUID; + +/** + * custom token processor, support CustomTokenSubject + * when token Expired and can refresh, return refresh token value + * + * @author tomsun28 + * @date 2020-12-03 20:37 + */ +public class CustomTokenProcessor extends BaseProcessor { + + private static final Logger logger = LoggerFactory.getLogger(CustomTokenProcessor.class); + private static final String TOKEN_SPLIT = "--"; + private static final int START_TIME_INDEX = 1; + private static final int PERIOD_TIME_INDEX = 2; + private static final int DOUBLE_TIME = 2; + + private SurenessAccountProvider accountProvider; + + @Override + public boolean canSupportSubjectClass(Class var) { + return var == CustomTokenSubject.class; + } + + @Override + public Class getSupportSubjectClass() { + return CustomTokenSubject.class; + } + + @Override + @SuppressWarnings("unchecked") + public Subject authenticated(Subject var) throws SurenessAuthenticationException { + String token = (String) var.getCredential(); + String[] tokenArr = token.split(TOKEN_SPLIT); + if (TokenStorage.matchToken(tokenArr[0], token)) { + // auth passed + String appId = tokenArr[0]; + SurenessAccount account = accountProvider.loadAccount(appId); + return CustomTokenSubject.builder(var) + .setPrincipal(appId) + .setOwnRoles(account.getOwnRoles()).build(); + + } else { + // token expired or not exist, if token can refresh, refresh it + // if expired time is not longer than refreshPeriodTime/2 , it can refresh + if (Long.parseLong(tokenArr[START_TIME_INDEX]) + (Long.parseLong(tokenArr[PERIOD_TIME_INDEX]) * DOUBLE_TIME) + >= System.currentTimeMillis()) { + long refreshPeriodTime = 36000L; + String refreshToken = tokenArr[0] + TOKEN_SPLIT + System.currentTimeMillis() + + TOKEN_SPLIT + refreshPeriodTime + + TOKEN_SPLIT + UUID.randomUUID().toString().replace("-", ""); + TokenStorage.addToken(tokenArr[0], refreshToken); + throw new RefreshExpiredTokenException(refreshToken); + } else if (Long.parseLong(tokenArr[START_TIME_INDEX]) + Long.parseLong(tokenArr[PERIOD_TIME_INDEX]) + <= System.currentTimeMillis()) { + if (logger.isDebugEnabled()) { + logger.debug("CustomTokenProcessor authenticated expired"); + } + throw new IncorrectCredentialsException("the token authenticated expired, please get new one"); + } else { + if (logger.isDebugEnabled()) { + logger.debug("CustomTokenProcessor authenticated fail"); + } + throw new IncorrectCredentialsException("the token authenticated error"); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void authorized(Subject var) throws SurenessAuthorizationException { + List ownRoles = (List) var.getOwnRoles(); + List supportRoles = (List) var.getSupportRoles(); + // if null, note that not config this resource + if (supportRoles == null) { + return; + } + // if config, ownRole must contain the supportRole item + if (ownRoles != null && supportRoles.stream().anyMatch(ownRoles::contains)) { + return; + } + throw new UnauthorizedException("custom authorized: do not have the role to access resource"); + } + + public void setAccountProvider(SurenessAccountProvider accountProvider) { + this.accountProvider = accountProvider; + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/processor/RefreshExpiredTokenException.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/processor/RefreshExpiredTokenException.java new file mode 100644 index 00000000..76f9c335 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/processor/RefreshExpiredTokenException.java @@ -0,0 +1,14 @@ +package sureness.sureness.processor; + +import com.usthe.sureness.processor.exception.SurenessAuthenticationException; + +/** + * refresh token message + * @author tomsun28 + * @date 2020-12-03 23:29 + */ +public class RefreshExpiredTokenException extends SurenessAuthenticationException { + public RefreshExpiredTokenException(String message) { + super(message); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/provider/DatabaseAccountProvider.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/provider/DatabaseAccountProvider.java new file mode 100644 index 00000000..0e3832fb --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/provider/DatabaseAccountProvider.java @@ -0,0 +1,25 @@ +package sureness.sureness.provider; + +import com.usthe.sureness.provider.SurenessAccount; +import com.usthe.sureness.provider.SurenessAccountProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import sureness.service.AccountService; + +/** + * the provider provides account info + * load account info from database + * @author tomsun28 + * @date 22:44 2020-03-02 + */ +@Component +public class DatabaseAccountProvider implements SurenessAccountProvider { + + @Autowired + AccountService accountService; + + @Override + public SurenessAccount loadAccount(String appId) { + return accountService.loadAccount(appId); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/provider/DatabasePathTreeProvider.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/provider/DatabasePathTreeProvider.java new file mode 100644 index 00000000..05c10171 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/provider/DatabasePathTreeProvider.java @@ -0,0 +1,34 @@ +package sureness.sureness.provider; + +import com.usthe.sureness.matcher.PathTreeProvider; +import com.usthe.sureness.util.SurenessCommonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import sureness.service.ResourceService; + +import java.util.Set; + +/** + * ths provider provides path resources + * load sureness config resource form database + * @author tomsun28 + * @date 16:00 2019-08-04 + */ +@Component +public class DatabasePathTreeProvider implements PathTreeProvider { + + @Autowired + private ResourceService resourceService; + + @Override + public Set providePathData() { + return SurenessCommonUtil.attachContextPath(getContextPath(), resourceService.getAllEnableResourcePath()); + + } + + @Override + public Set provideExcludedResource() { + return SurenessCommonUtil.attachContextPath(getContextPath(), resourceService.getAllDisableResourcePath()); + } + +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomPasswdSubjectCreator.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomPasswdSubjectCreator.java new file mode 100644 index 00000000..c1f73bc0 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomPasswdSubjectCreator.java @@ -0,0 +1,54 @@ +package sureness.sureness.subject; + +import com.usthe.sureness.subject.Subject; +import com.usthe.sureness.subject.SubjectCreate; +import com.usthe.sureness.subject.support.PasswordSubject; +import org.springframework.http.server.reactive.ServerHttpRequest; + + +/** + * custom subject creator + * A custom creator is demonstrated here + * In addition to the basic auth method, we may obtain our account password from other places for authentication. + * eg: username and password in header + * header { + * "username": "userTom", + * "password": "123456" + * } + * Here we define a creator to create PasswordSubject from this request header like above. + * @author tomsun28 + * @date 22:59 2020-03-02 + */ +public class CustomPasswdSubjectCreator implements SubjectCreate { + + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + + @Override + public boolean canSupportSubject(Object context) { + // define which request can be access + if (context instanceof ServerHttpRequest) { + String username = ((ServerHttpRequest)context).getHeaders().getFirst("Username"); + String password = ((ServerHttpRequest)context).getHeaders().getFirst("Password"); + return username != null && password != null; + } else { + return false; + } + } + + @Override + public Subject createSubject(Object context) { + // create PasswordSubject from request + String username = ((ServerHttpRequest)context).getHeaders().getFirst("Username"); + String password = ((ServerHttpRequest)context).getHeaders().getFirst("Password"); + + String remoteHost = ((ServerHttpRequest) context).getRemoteAddress().getHostString(); + String requestUri = ((ServerHttpRequest) context).getURI().getPath(); + String requestType = ((ServerHttpRequest) context).getMethod().toString(); + String targetUri = requestUri.concat("===").concat(requestType).toLowerCase(); + return PasswordSubject.builder(username, password) + .setRemoteHost(remoteHost) + .setTargetResource(targetUri) + .build(); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomTokenSubject.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomTokenSubject.java new file mode 100644 index 00000000..df8462d3 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomTokenSubject.java @@ -0,0 +1,163 @@ +package sureness.sureness.subject; + +import com.usthe.sureness.subject.PrincipalMap; +import com.usthe.sureness.subject.Subject; + +import java.util.List; + +/** + * custom define token subject + * @author tomsun28 + * @date 2020-12-03 22:08 + */ +public class CustomTokenSubject implements Subject { + + private static final long serialVersionUID = 1L; + + /** user identifier **/ + private String appId; + + /** token : admin--issueTime--refreshPeriodTime--uuid **/ + private String token; + + /** remote ip **/ + private String remoteHost; + + /** remote device **/ + private String userAgent; + + /** the roles which this user owned **/ + private List ownRoles; + + /** the uri resource which this user want access **/ + private String targetUri; + + /** the Roles which can access this resource above-targetUri **/ + private List supportRoles; + + private CustomTokenSubject(Builder builder) { + this.appId = builder.appId; + this.token = builder.token; + this.remoteHost = builder.remoteHost; + this.userAgent = builder.userAgent; + this.ownRoles = builder.ownRoles; + this.supportRoles = builder.supportRoles; + this.targetUri = builder.targetUri; + } + + @Override + public Object getPrincipal() { + return this.appId; + } + + @Override + public PrincipalMap getPrincipalMap() { + return null; + } + + @Override + public Object getCredential() { + return this.token; + } + + @Override + public Object getOwnRoles() { + return this.ownRoles; + } + + @Override + public Object getTargetResource() { + return this.targetUri; + } + + @Override + public Object getSupportRoles() { + return this.supportRoles; + } + + @SuppressWarnings("unchecked") + @Override + public void setSupportRoles(Object var1) { + this.supportRoles = (List) var1; + } + + public String getRemoteHost() { + return remoteHost; + } + + public String getUserAgent() { + return userAgent; + } + + public static Builder builder(String token) { + return new Builder(token); + } + + public static Builder builder(Subject subject) { + return new Builder(subject); + } + + public static class Builder { + + private String appId; + private String token; + private String remoteHost; + private String userAgent; + private List ownRoles; + private String targetUri; + private List supportRoles; + + public Builder(String token) { + this.token = token; + } + + @SuppressWarnings("unchecked") + public Builder(Subject subject) { + this.appId = String.valueOf(subject.getPrincipal()); + this.token = String.valueOf(subject.getCredential()); + this.ownRoles = (List) subject.getOwnRoles(); + this.targetUri = String.valueOf(subject.getTargetResource()); + this.supportRoles = (List) subject.getSupportRoles(); + } + + public Builder setPrincipal(String appId) { + this.appId = appId; + return this; + } + + public Builder setCredentials(String token) { + this.token = token; + return this; + } + + public Builder setTargetResource(String targetUri) { + this.targetUri = targetUri; + return this; + } + + public Builder setOwnRoles(List ownRoles) { + this.ownRoles = ownRoles; + return this; + } + + public Builder setSupportRoles(List supportRoles) { + this.supportRoles = supportRoles; + return this; + } + + public Builder setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + return this; + } + + public Builder setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public CustomTokenSubject build() { + return new CustomTokenSubject(this); + } + + } +} diff --git a/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomTokenSubjectCreator.java b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomTokenSubjectCreator.java new file mode 100644 index 00000000..68a76959 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/java/sureness/sureness/subject/CustomTokenSubjectCreator.java @@ -0,0 +1,45 @@ +package sureness.sureness.subject; + +import com.usthe.sureness.subject.Subject; +import com.usthe.sureness.subject.SubjectCreate; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.util.Objects; + + +/** + * custom token creator, get token from http request header - {"Token" : "tokenValue"} + * tokenValue is : admin--issueTime--refreshPeriodTime--uuid + * @author tomsun28 + * @date 2020-12-03 22:08 + */ +public class CustomTokenSubjectCreator implements SubjectCreate { + + private static final String TOKEN_SPLIT = "--"; + + private static final int TOKEN_SPLIT_SIZE = 4; + + @Override + public boolean canSupportSubject(Object context) { + // support token + // {"Token" : "tokenValue"} + if (context instanceof ServerHttpRequest) { + String authorization = ((ServerHttpRequest)context).getHeaders().getFirst("Token"); + return authorization != null && authorization.split(TOKEN_SPLIT).length == TOKEN_SPLIT_SIZE; + } + return false; + } + + @Override + public Subject createSubject(Object context) { + String authorization = ((ServerHttpRequest)context).getHeaders().getFirst("Token"); + String remoteHost = Objects.requireNonNull(((ServerHttpRequest) context).getRemoteAddress()).getHostString(); + String requestUri = ((ServerHttpRequest) context).getURI().getPath(); + String requestType = ((ServerHttpRequest) context).getMethodValue(); + String targetUri = requestUri.concat("===").concat(requestType.toLowerCase()); + return CustomTokenSubject.builder(authorization) + .setRemoteHost(remoteHost) + .setTargetResource(targetUri) + .build(); + } +} diff --git a/samples/shenyu-sureness-sql/src/main/resources/application.yml b/samples/shenyu-sureness-sql/src/main/resources/application.yml new file mode 100644 index 00000000..b1cbcce4 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/resources/application.yml @@ -0,0 +1,54 @@ +server: + port: 8081 +spring: + profiles: + active: dev + main: + allow-bean-definition-overriding: true +soul: + sync: + websocket: + urls: ws://localhost:9095/websocket + +#jasypt: +# encryptor: +# password: e!swhdg&d763jo + +--- +spring: + profiles: dev + datasource: + driver-class-name: com.mysql.jdbc.Driver + username: username + password: password + url: jdbc:mysql://localhost:3306/tom?useUnicode=true&characterEncoding=utf-8&useSSL=false + platform: mysql + schema: classpath:db/schema.sql + data: classpath:db/data.sql + # 每次重启都会根据data.sql schema.sql重建表数据, 设置 NEVER 就不启动重建 + # Each restart will rebuild the table data according to data.sql schema.sql + # setting NEVER will not start the reconstruction + initialization-mode: always + jackson: + serialization: + FAIL_ON_EMPTY_BEANS: false + jpa: + database: mysql + show-sql: true + + +--- +spring: + profiles: prod + datasource: + driver-class-name: com.mysql.jdbc.Driver + username: username + password: password + url: jdbc:mysql://localhost:3306/tom?useUnicode=true&characterEncoding=utf-8&useSSL=false + platform: mysql + schema: classpath:db/schema.sql + data: classpath:db/data.sql + initialization-mode: always + jpa: + database: mysql + show-sql: true diff --git a/samples/shenyu-sureness-sql/src/main/resources/db/data.sql b/samples/shenyu-sureness-sql/src/main/resources/db/data.sql new file mode 100644 index 00000000..34eea684 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/resources/db/data.sql @@ -0,0 +1,71 @@ +use tom ; +-- ---------------------------- +-- Records of auth_resource +-- ---------------------------- +insert into auth_resource (id, name, code, uri, type, method, status, description) values (101, 'User get token', 'ACCOUNT_TOKEN', '/auth/token', 'account', 'POST', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (102, 'User register', 'ACCOUNT_REGISTER', '/auth/register', 'account', 'POST', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (103, 'Add resource', 'ADD_RESOURCE', '/resource', 'resource', 'POST', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (104, 'Update resource', 'UPDATE_RESOURCE', '/resource', 'resource', 'PUT', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (105, 'Delete resource', 'DELETE_RESOURCE', '/resource/*', 'resource', 'DELETE', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (106, 'Get resource', 'GET_RESOURCES', '/resource/*/*', 'resource', 'GET', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (107, 'Add role', 'ADD_ROLE', '/role', 'role', 'POST', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (108, 'Update role', 'UPDATE_ROLE', '/role', 'role', 'PUT', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (109, 'Delete role', 'DELETE_ROLE', '/role/*', 'role', 'DELETE', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (110, 'Get role', 'GET_ROLES', '/role/*/*', 'role', 'GET', 1, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (111, 'User get custom token', 'ACCOUNT_CUSTOM_TOKEN', '/auth/custom/token', 'account', 'POST', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (112, 'Static Resource', 'Static Resource', '/**/*.html', 'static', 'GET', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (113, 'Static Resource', 'Static Resource', '/**/*.js', 'static', 'GET', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (114, 'Static Resource', 'Static Resource', '/**/*.css', 'static', 'GET', 9, null); + +-- ---------------------------- +-- Records of auth_role +-- ---------------------------- +insert into auth_role (id, name, code, status, description) values (100, 'admin role', 'role_admin', 1, null); +insert into auth_role (id, name, code, status, description) values (102, 'user role', 'role_user', 1, null); +insert into auth_role (id, name, code, status, description) values (103, 'guest role', 'role_guest', 1, null); + +-- ---------------------------- +-- Records of auth_role_resource_bind +-- ---------------------------- +-- role_admin has these resource +insert into auth_role_resource_bind (id, role_id, resource_id) values (1, 100, 101); +insert into auth_role_resource_bind (id, role_id, resource_id) values (2, 100, 102); +insert into auth_role_resource_bind (id, role_id, resource_id) values (3, 100, 103); +insert into auth_role_resource_bind (id, role_id, resource_id) values (4, 100, 104); +insert into auth_role_resource_bind (id, role_id, resource_id) values (5, 100, 105); +insert into auth_role_resource_bind (id, role_id, resource_id) values (6, 100, 106); +insert into auth_role_resource_bind (id, role_id, resource_id) values (7, 100, 107); +insert into auth_role_resource_bind (id, role_id, resource_id) values (8, 100, 108); +insert into auth_role_resource_bind (id, role_id, resource_id) values (9, 100, 109); +insert into auth_role_resource_bind (id, role_id, resource_id) values (10, 100, 110); + +-- role_user has "get,add,update" related resources, do not have "delete" related resources +insert into auth_role_resource_bind (id, role_id, resource_id) values (11, 102, 103); +insert into auth_role_resource_bind (id, role_id, resource_id) values (12, 102, 104); +insert into auth_role_resource_bind (id, role_id, resource_id) values (13, 102, 106); +insert into auth_role_resource_bind (id, role_id, resource_id) values (14, 102, 107); +insert into auth_role_resource_bind (id, role_id, resource_id) values (15, 102, 108); +insert into auth_role_resource_bind (id, role_id, resource_id) values (16, 102, 110); + +-- role_guest has "get" related resources +insert into auth_role_resource_bind (id, role_id, resource_id) values (17, 103, 106); +insert into auth_role_resource_bind (id, role_id, resource_id) values (18, 103, 110); + + +-- ---------------------------- +-- Records of auth_user +-- ---------------------------- +insert into auth_user (id, username, password, salt, avatar, phone, email, sex, status, create_where) values (111, 'admin', 'admin', null, null, null, null, null, 1, 1); +insert into auth_user (id, username, password, salt, avatar, phone, email, sex, status, create_where) values (112, 'user', 'user', null, null, null, null, null, 1, 1); +insert into auth_user (id, username, password, salt, avatar, phone, email, sex, status, create_where) values (113, 'guest', 'guest', null, null, null, null, null, 1, 1); +insert into auth_user (id, username, password, salt, avatar, phone, email, sex, status, create_where) values (114, 'noRole', 'noRole', null, null, null, null, null, 1, 1); + + + +-- ---------------------------- +-- Records of auth_user_role_bind +-- ---------------------------- +insert into auth_user_role_bind (id, user_id, role_id) values (12, 111, 100); +insert into auth_user_role_bind (id, user_id, role_id) values (13, 111, 102); +insert into auth_user_role_bind (id, user_id, role_id) values (14, 112, 102); +insert into auth_user_role_bind (id, user_id, role_id) values (15, 113, 103); \ No newline at end of file diff --git a/samples/shenyu-sureness-sql/src/main/resources/db/schema.sql b/samples/shenyu-sureness-sql/src/main/resources/db/schema.sql new file mode 100644 index 00000000..a238c9d6 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/resources/db/schema.sql @@ -0,0 +1,88 @@ +use tom; + +-- ---------------------------- +-- Table structure for auth_resource +-- ---------------------------- +DROP TABLE IF EXISTS auth_resource ; +CREATE TABLE auth_resource +( + id bigint not null auto_increment comment 'resource ID', + name varchar(50) not null comment 'resource name', + code varchar(50) not null comment 'resource code', + uri varchar(255) not null comment 'access URL', + type varchar(20) comment 'resource type', + method varchar(10) not null comment 'access method: GET POST PUT DELETE PATCH', + status smallint(4) not null default 1 comment 'status 1:normal、9:filtering(Exclusion protection-all can access this api)', + description varchar(255) comment 'resource description', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key (id) +) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Table structure for auth_role +-- ---------------------------- +DROP TABLE IF EXISTS auth_role ; +CREATE TABLE auth_role +( + id bigint not null auto_increment comment 'role ID', + name varchar(50) not null comment 'role name', + code varchar(50) not null comment 'role code', + status smallint(4) not null default 1 comment 'status 1:normal、9:disable', + description varchar(255) comment 'role description', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key ( id ) +) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Table structure for auth_role_resource_bind +-- ---------------------------- +DROP TABLE IF EXISTS auth_role_resource_bind; +CREATE TABLE auth_role_resource_bind +( + id bigint not null auto_increment comment 'ID', + role_id bigint not null comment 'role ID', + resource_id bigint not null comment 'resource ID', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key (id), + unique key unique_bind (role_id, resource_id) +) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Table structure for auth_user +-- ---------------------------- +DROP TABLE IF EXISTS auth_user ; +CREATE TABLE auth_user +( + id bigint not null auto_increment comment 'ID', + username varchar(50) not null comment 'username(nick_name)', + password varchar(50) not null comment 'password=MD5(passwd+salt)', + salt varchar(20) comment 'salt', + avatar varchar(100) comment 'avatar', + phone varchar(20) comment 'phone number(unique)', + email varchar(50) comment 'email(unique)', + sex tinyint(4) comment 'sex(1.man 2.woman)', + status tinyint(4) not null default 1 comment 'account status(1.normal 2.locked 3.deleted 4.illegal)', + create_where tinyint(4) comment 'create where(1.web 2.android 3.ios 4.win 5.mac 6.linux)', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key (id), + unique (username, phone, email) +) ENGINE = InnoDB DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Table structure for auth_user_role_bind +-- ---------------------------- +DROP TABLE IF EXISTS auth_user_role_bind ; +CREATE TABLE auth_user_role_bind +( + id bigint not null auto_increment comment 'ID', + user_id bigint not null comment 'user ID', + role_id bigint not null comment 'role ID', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key (id), + unique key unique_bind (user_id, role_id) +) ENGINE = InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/samples/shenyu-sureness-sql/src/main/resources/sureness.yml b/samples/shenyu-sureness-sql/src/main/resources/sureness.yml new file mode 100644 index 00000000..c5087610 --- /dev/null +++ b/samples/shenyu-sureness-sql/src/main/resources/sureness.yml @@ -0,0 +1,22 @@ +## -- sureness.yml document dataSource-- ## + +# load api resource which need be protected, config role who can access these resource. +# resources that are not configured are also authenticated and protected by default, but not authorized +# eg: /api/v2/host===post===[role2,role3,role4] means /api/v2/host===post can be access by role2,role3,role4 +# eg: /api/v1/getSource3===get===[] means /api/v1/getSource3===get can not be access by any role +resourceRole: + - /api/v2/host===post===[role2,role3,role4] + - /api/v1/getSource3===get===[] + + +# load api resource which do not need be protected, means them need be excluded. +# these api resource can be access by everyone +excludedResource: + - /api/v3/host===get + - /api/v3/book===get + - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get + - /**/*.ico===get + - /**/*.png===* diff --git a/samples/shenyu-sureness/pom.xml b/samples/shenyu-sureness/pom.xml new file mode 100644 index 00000000..0c0ef605 --- /dev/null +++ b/samples/shenyu-sureness/pom.xml @@ -0,0 +1,77 @@ + + + + samples + com.usthe.sureness + 1.0.0-SNAPSHOT + + 4.0.0 + + shenyu-sureness + + + 1.8 + 2020.0.2 + 1.0.3 + + + + + org.yaml + snakeyaml + 1.25 + + + com.usthe.sureness + sureness-core + 1.0.3 + + + org.springframework.boot + spring-boot-starter-webflux + 2.2.2.RELEASE + + + org.springframework.boot + spring-boot-starter-actuator + 2.2.2.RELEASE + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.dromara + soul-spring-boot-starter-gateway + 2.3.0-RELEASE + + + org.springframework.boot + spring-boot-starter-web + + + + + + org.dromara + soul-spring-boot-starter-sync-data-websocket + 2.3.0-RELEASE + + + org.dromara + soul-spring-boot-starter-plugin-httpclient + 2.3.0-RELEASE + + + org.dromara + soul-spring-boot-starter-plugin-divide + 2.3.0-RELEASE + + + + diff --git a/samples/shenyu-sureness/src/main/java/gateway/ShenyuApplication.java b/samples/shenyu-sureness/src/main/java/gateway/ShenyuApplication.java new file mode 100644 index 00000000..d2a1180f --- /dev/null +++ b/samples/shenyu-sureness/src/main/java/gateway/ShenyuApplication.java @@ -0,0 +1,18 @@ +package gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @title: ShenyuApplication + * @Author hqgordon + * @Date: 2021/7/31 9:05 上午 + * @Description: + * @Version 1.0 + */ +@SpringBootApplication +public class ShenyuApplication { + public static void main(String[] args) { + SpringApplication.run(ShenyuApplication.class,args); + } +} \ No newline at end of file diff --git a/samples/shenyu-sureness/src/main/java/gateway/shenyu/SurenessConfiguration.java b/samples/shenyu-sureness/src/main/java/gateway/shenyu/SurenessConfiguration.java new file mode 100644 index 00000000..5b3ecadc --- /dev/null +++ b/samples/shenyu-sureness/src/main/java/gateway/shenyu/SurenessConfiguration.java @@ -0,0 +1,25 @@ +package gateway.shenyu; + +import com.usthe.sureness.DefaultSurenessConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @title: SurenessConfiguration + * @Author hqgordon + * @Date: 2021/7/31 5:13 下午 + * @Description: 配置类 + * @Version 1.0 + */ +@Configuration +public class SurenessConfiguration { + /** + * new sureness default config bean + * @return default config bean + */ + @Bean + public DefaultSurenessConfig surenessConfig() { + return new DefaultSurenessConfig(DefaultSurenessConfig.SUPPORT_SPRING_REACTIVE); + } + +} diff --git a/samples/shenyu-sureness/src/main/java/gateway/shenyu/SurenessFilter.java b/samples/shenyu-sureness/src/main/java/gateway/shenyu/SurenessFilter.java new file mode 100644 index 00000000..708759e2 --- /dev/null +++ b/samples/shenyu-sureness/src/main/java/gateway/shenyu/SurenessFilter.java @@ -0,0 +1,102 @@ +package gateway.shenyu; + +import com.usthe.sureness.mgt.SurenessSecurityManager; +import com.usthe.sureness.processor.exception.*; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.SurenessContextHolder; +import io.netty.buffer.UnpooledByteBufAllocator; +import lombok.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +import javax.annotation.Nonnull; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + +/** + * @title: SurenessConfiguration + * @Author hqgordon + * @Date: 2021/7/31 5:13 下午 + * @Description: sureness过滤器 + * @Version 1.0 + */ +@Component +@Order(-1) +public class SurenessFilter implements WebFilter { + + private static final Logger logger = LoggerFactory.getLogger(SurenessFilter.class); + /* + * @Author hqgordon + * @Description //TODO + * @Date 2021/8/4 8:38 上午 + * @Param [exchange, chain] + * @return reactor.core.publisher.Mono + **/ + @Override + @Nonnull + public Mono filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + try { + SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(request); + // You can consider using SurenessContextHolder to bind subject in threadLocal + // if bind, please remove it when end + if (subject != null) { + SurenessContextHolder.bindSubject(subject); + } + } catch (IncorrectCredentialsException | UnknownAccountException | ExpiredCredentialsException e1) { + logger.debug("this request is illegal"); + return responseWrite(exchange, HttpStatus.UNAUTHORIZED, e1.getMessage(),null); + } catch (NeedDigestInfoException e5) { + logger.debug("you should try once again with digest auth information"); + return responseWrite(exchange, HttpStatus.UNAUTHORIZED, + "try once again with digest auth information", + Collections.singletonMap("WWW-Authenticate", e5.getAuthenticate())); + } catch (UnauthorizedException e5) { + logger.debug("this account can not access this resource"); + return responseWrite(exchange, HttpStatus.FORBIDDEN, e5.getMessage(),null); + } catch (RuntimeException e) { + logger.error("other exception happen: ", e); + return responseWrite(exchange, HttpStatus.FORBIDDEN, e.getMessage(),null); + } + return chain.filter(exchange).doFinally(x -> SurenessContextHolder.unbindSubject()); + + } + /* + * @Author hqgordon + * @Description //TODO + * @Date 2021/8/4 8:38 上午 + * @Param [exchange, statusCode, message, headers] + * @return reactor.core.publisher.Mono + **/ + private Mono responseWrite(ServerWebExchange exchange, HttpStatus statusCode, + String message, Map headers) { + exchange.getResponse().setStatusCode(statusCode); + if (headers != null) { + headers.forEach((key, value) -> exchange.getResponse().getHeaders().add(key, value)); + } + if (message != null) { + return exchange.getResponse().writeWith(Flux.create(sink -> { + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false)); + DataBuffer dataBuffer= nettyDataBufferFactory.wrap(message.getBytes(StandardCharsets.UTF_8)); + sink.next(dataBuffer); + sink.complete(); + })); + } else { + return exchange.getResponse().setComplete(); + } + } + +} diff --git a/samples/shenyu-sureness/src/main/resources/application.yml b/samples/shenyu-sureness/src/main/resources/application.yml new file mode 100644 index 00000000..f3fcd838 --- /dev/null +++ b/samples/shenyu-sureness/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + main: + allow-bean-definition-overriding: true +management: + health: + defaults: + enabled: false +soul: + sync: + websocket: + urls: ws://localhost:9095/websocket +server: + port: 9195 diff --git a/samples/shenyu-sureness/src/main/resources/sureness.yml b/samples/shenyu-sureness/src/main/resources/sureness.yml new file mode 100644 index 00000000..7a1f9c18 --- /dev/null +++ b/samples/shenyu-sureness/src/main/resources/sureness.yml @@ -0,0 +1,43 @@ +## -- sureness.yml document dataSource-- ## + +# load api resource which need be protected, config role who can access these resource. +# resources that are not configured are also authenticated and protected by default, but not authorized +# eg: /api/v1/bar===post===[role1] means /api/v1/bar===post can be access by role1 +# eg: /api/v3/foo===get===[] means /api/v3/foo===* can not be access by any role +resourceRole: + - /api/v1/bar/*===get===[role1,role2,role3] + - /api/v1/bar===post===[role1] + - /api/v2/bar===put===[role2] + - /api/v2/foo===get===[role3] + - /api/v3/foo===get===[] + +# load api resource which do not need be protected, means them need be excluded. +# these api resource can be access by everyone +# eg: /**/*.png===* means get/post/put/delete/patch any url suffixed with `.png` can be access by everyone +excludedResource: + - /api/v2/foo===delete + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get + - /**/*.ico===* + - /**/*.png===* + + +# account info +# there are three account: sam, tom, lisa +# eg: sam has [role1,role2,role3], password is sam123, has salt -> 123 +# eg: tom has role2, password is tom123 +# eg: lisa has role3, password is lisa123 +account: + - appId: sam + # if add salt, the credential is encrypted by md5 - result is: MD5(password+salt) + # digest auth not support encrypted credential + credential: 1B9E9136628CB1B161AE47132DD7AF19 + salt: 123 + role: [role1,role2,role3] + - appId: tom + credential: tom123 + role: [role2] + - appId: lisa + credential: lisa123 + role: [role3] \ No newline at end of file diff --git a/samples/spring-gateway-sureness/src/main/java/gateway/sureness/SurenessFilter.java b/samples/spring-gateway-sureness/src/main/java/gateway/sureness/SurenessFilter.java index 0d0f4cfe..958c022e 100644 --- a/samples/spring-gateway-sureness/src/main/java/gateway/sureness/SurenessFilter.java +++ b/samples/spring-gateway-sureness/src/main/java/gateway/sureness/SurenessFilter.java @@ -72,7 +72,7 @@ public int getOrder() { * @param message message */ private Mono responseWrite(ServerWebExchange exchange, HttpStatus statusCode, - String message, Map headers) { + String message, Map headers) { exchange.getResponse().setStatusCode(statusCode); if (headers != null) { @@ -80,11 +80,11 @@ private Mono responseWrite(ServerWebExchange exchange, HttpStatus statusCo } if (message != null) { return exchange.getResponse().writeWith(Flux.create(sink -> { - NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false)); - DataBuffer dataBuffer= nettyDataBufferFactory.wrap(message.getBytes(StandardCharsets.UTF_8)); - sink.next(dataBuffer); - sink.complete(); - })); + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false)); + DataBuffer dataBuffer= nettyDataBufferFactory.wrap(message.getBytes(StandardCharsets.UTF_8)); + sink.next(dataBuffer); + sink.complete(); + })); } else { return exchange.getResponse().setComplete(); } diff --git a/samples/spring-gateway-sureness/src/main/resources/application.yml b/samples/spring-gateway-sureness/src/main/resources/application.yml index be28e23d..736b2202 100644 --- a/samples/spring-gateway-sureness/src/main/resources/application.yml +++ b/samples/spring-gateway-sureness/src/main/resources/application.yml @@ -7,4 +7,4 @@ spring: - id: route1 uri: https://www.bing.com predicates: - - Path=/api/** + - Path=/api/**