diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/remoteapi/AuthCenterRemoteApi.java b/api-portal/src/main/java/com/jmsoftware/apiportal/remoteapi/AuthCenterRemoteApi.java index 42f7d162..45924a88 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/remoteapi/AuthCenterRemoteApi.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/remoteapi/AuthCenterRemoteApi.java @@ -1,6 +1,9 @@ package com.jmsoftware.apiportal.remoteapi; +import com.jmsoftware.apiportal.universal.aspect.ValidateArgument; import com.jmsoftware.common.bean.ResponseBodyBean; +import com.jmsoftware.common.domain.authcenter.permission.GetPermissionListByRoleIdListPayload; +import com.jmsoftware.common.domain.authcenter.permission.GetPermissionListByRoleIdListResponse; import com.jmsoftware.common.domain.authcenter.role.GetRoleListByUserIdPayload; import com.jmsoftware.common.domain.authcenter.role.GetRoleListByUserIdResponse; import com.jmsoftware.common.domain.authcenter.user.GetUserByLoginTokenPayload; @@ -11,6 +14,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import javax.validation.Valid; + /** *

AuthCenterRemoteApi

*

@@ -27,8 +32,9 @@ public interface AuthCenterRemoteApi { * @param payload the payload * @return the user by login token */ + @ValidateArgument @PostMapping("/user-remote-api/get-user-by-login-token") - ResponseBodyBean getUserByLoginToken(@RequestBody GetUserByLoginTokenPayload payload); + ResponseBodyBean getUserByLoginToken(@Valid @RequestBody GetUserByLoginTokenPayload payload); /** * Gets role list by user id. @@ -36,8 +42,9 @@ public interface AuthCenterRemoteApi { * @param payload the payload * @return the role list by user id */ + @ValidateArgument @PostMapping("/role-remote-api/get-role-list-by-user-id") - ResponseBodyBean getRoleListByUserId(@RequestBody GetRoleListByUserIdPayload payload); + ResponseBodyBean getRoleListByUserId(@Valid @RequestBody GetRoleListByUserIdPayload payload); /** * Save user for registering response body bean. @@ -45,6 +52,17 @@ public interface AuthCenterRemoteApi { * @param payload the payload * @return the response body bean */ + @ValidateArgument @PostMapping("/user-remote-api/save-user-for-registering") - ResponseBodyBean saveUserForRegistering(@RequestBody SaveUserForRegisteringPayload payload); + ResponseBodyBean saveUserForRegistering(@Valid @RequestBody SaveUserForRegisteringPayload payload); + + /** + * Gets permission list by role id list. + * + * @param payload the payload + * @return the permission list by role id list + */ + @ValidateArgument + @PostMapping("/permission-remote-api/get-permission-list-by-role-id-list") + ResponseBodyBean getPermissionListByRoleIdList(@Valid @RequestBody GetPermissionListByRoleIdListPayload payload); } diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/SecurityHandlerConfiguration.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/SecurityHandlerConfiguration.java index e864f741..55843db7 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/SecurityHandlerConfiguration.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/SecurityHandlerConfiguration.java @@ -2,8 +2,10 @@ import com.jmsoftware.common.constant.HttpStatus; import com.jmsoftware.common.util.ResponseUtil; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; /** @@ -14,12 +16,24 @@ * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com * @date 5/2/20 11:41 PM **/ +@Slf4j @Configuration public class SecurityHandlerConfiguration { + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return ((request, response, authException) -> { + log.error("Authentication encountered an exception! Exception message: {}", authException.getMessage(), + authException); + ResponseUtil.renderJson(response, HttpStatus.FORBIDDEN, null); + }); + } + @Bean public AccessDeniedHandler accessDeniedHandler() { - return (request, response, accessDeniedException) -> ResponseUtil.renderJson(response, - HttpStatus.FORBIDDEN, - null); + return ((request, response, accessDeniedException) -> { + log.error("Access was denied! Exception message: {}", accessDeniedException.getMessage(), + accessDeniedException); + ResponseUtil.renderJson(response, HttpStatus.FORBIDDEN, null); + }); } } diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java index 0fc8304c..89eb232c 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/configuration/WebSecurityConfiguration.java @@ -16,6 +16,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -36,6 +37,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { private final CustomConfiguration customConfiguration; private final AccessDeniedHandler accessDeniedHandler; + private final AuthenticationEntryPoint authenticationEntryPoint; private final CustomUserDetailsServiceImpl customUserDetailsServiceImpl; private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -83,7 +85,10 @@ protected void configure(HttpSecurity http) throws Exception { .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Exception handling - .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler); + .and() + .exceptionHandling() + .accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); // Add customized JWT filter http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/domain/UserPrincipal.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/domain/UserPrincipal.java index e861e9a0..24988673 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/domain/UserPrincipal.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/domain/UserPrincipal.java @@ -2,6 +2,7 @@ import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.jmsoftware.common.domain.authcenter.permission.GetPermissionListByRoleIdListResponse; import com.jmsoftware.common.domain.authcenter.user.GetUserByLoginTokenResponse; import com.jmsoftware.common.domain.authcenter.user.UserStatus; import lombok.AllArgsConstructor; @@ -94,7 +95,7 @@ public class UserPrincipal implements UserDetails { * @return user principal */ public static UserPrincipal create(GetUserByLoginTokenResponse user, List roleNameList, - List permissionList) { + List permissionList) { val authorities = permissionList.stream() .filter(permission -> StrUtil.isNotBlank(permission.getPermissionExpression())) diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java index c142856d..89adf1fa 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/filter/JwtAuthenticationFilter.java @@ -81,9 +81,14 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse UserDetails userDetails; try { userDetails = customUserDetailsServiceImpl.loadUserByUsername(username); - } catch (UsernameNotFoundException e) { - log.error("Cannot find user by username: {}", username); - ResponseUtil.renderJson(response, HttpStatus.UNAUTHORIZED, null); + } catch (Exception e) { + log.error("Exception occurred when loading user by username! Exception message: {} Username: {}", + e.getMessage(), username); + if (e instanceof UsernameNotFoundException) { + ResponseUtil.renderJson(response, HttpStatus.UNAUTHORIZED, null); + return; + } + ResponseUtil.renderJson(response, HttpStatus.ERROR, e.getMessage()); return; } val authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CustomUserDetailsServiceImpl.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CustomUserDetailsServiceImpl.java index 7db8d817..ed843e61 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CustomUserDetailsServiceImpl.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/CustomUserDetailsServiceImpl.java @@ -3,10 +3,9 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import com.jmsoftware.apiportal.remoteapi.AuthCenterRemoteApi; -import com.jmsoftware.apiportal.universal.domain.PermissionPO; import com.jmsoftware.apiportal.universal.domain.UserPrincipal; -import com.jmsoftware.apiportal.universal.mapper.PermissionMapper; import com.jmsoftware.common.constant.HttpStatus; +import com.jmsoftware.common.domain.authcenter.permission.GetPermissionListByRoleIdListPayload; import com.jmsoftware.common.domain.authcenter.role.GetRoleListByUserIdPayload; import com.jmsoftware.common.domain.authcenter.role.GetRoleListByUserIdResponse; import com.jmsoftware.common.domain.authcenter.user.GetUserByLoginTokenPayload; @@ -19,7 +18,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import java.util.List; import java.util.stream.Collectors; /** @@ -33,7 +31,6 @@ @Service @RequiredArgsConstructor public class CustomUserDetailsServiceImpl implements UserDetailsService { - private final PermissionMapper permissionMapper; private final AuthCenterRemoteApi authCenterRemoteApi; @Override @@ -54,12 +51,14 @@ public UserDetails loadUserByUsername(String credentials) throws UsernameNotFoun if (CollUtil.isEmpty(roleList)) { throw new SecurityException(HttpStatus.ROLE_NOT_FOUND); } - List roleIdList = roleList.stream() - .map(GetRoleListByUserIdResponse.Role::getId) - .collect(Collectors.toList()); - List permissionList = permissionMapper.selectByRoleIdList(roleIdList); + val payload2 = new GetPermissionListByRoleIdListPayload(); + roleList.forEach(role -> { + payload2.getRoleIdList().add(role.getId()); + }); + val permissionListByRoleIdListResponse = authCenterRemoteApi.getPermissionListByRoleIdList(payload2); val roleNameList = roleList.stream().map(GetRoleListByUserIdResponse.Role::getName).collect(Collectors.toList()); - return UserPrincipal.create(data, roleNameList, permissionList); + return UserPrincipal.create(data, roleNameList, + permissionListByRoleIdListResponse.getData().getPermissionList()); } } diff --git a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/RbacAuthorityServiceImpl.java b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/RbacAuthorityServiceImpl.java index 88a04533..8020888d 100644 --- a/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/RbacAuthorityServiceImpl.java +++ b/api-portal/src/main/java/com/jmsoftware/apiportal/universal/service/impl/RbacAuthorityServiceImpl.java @@ -1,33 +1,33 @@ package com.jmsoftware.apiportal.universal.service.impl; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; +import com.jmsoftware.apiportal.remoteapi.AuthCenterRemoteApi; import com.jmsoftware.apiportal.universal.configuration.CustomConfiguration; -import com.jmsoftware.apiportal.universal.domain.PermissionPO; import com.jmsoftware.apiportal.universal.domain.PermissionType; import com.jmsoftware.apiportal.universal.domain.RolePO; import com.jmsoftware.apiportal.universal.domain.UserPrincipal; -import com.jmsoftware.apiportal.universal.service.PermissionService; import com.jmsoftware.apiportal.universal.service.RbacAuthorityService; import com.jmsoftware.apiportal.universal.service.RoleService; import com.jmsoftware.common.constant.HttpStatus; +import com.jmsoftware.common.domain.authcenter.permission.GetPermissionListByRoleIdListPayload; +import com.jmsoftware.common.domain.authcenter.permission.GetPermissionListByRoleIdListResponse; import com.jmsoftware.common.exception.SecurityException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Service; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; -import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; /** @@ -37,62 +37,62 @@ * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com * @date 2019-03-23 14:25 **/ +@Slf4j @Service @RequiredArgsConstructor public class RbacAuthorityServiceImpl implements RbacAuthorityService { + private final AuthCenterRemoteApi authCenterRemoteApi; + private final RoleService roleService; - private final PermissionService permissionService; private final RequestMappingHandlerMapping mapping; private final CustomConfiguration customConfiguration; private final JwtServiceImpl jwtServiceImpl; @Override public boolean hasPermission(HttpServletRequest request, Authentication authentication) { - String username = jwtServiceImpl.getUsernameFromRequest(request); + val username = jwtServiceImpl.getUsernameFromRequest(request); // Super user has no restriction on any requests. if (customConfiguration.getSuperUser().equals(username)) { return true; } - this.checkRequest(request); - - Object userInfo = authentication.getPrincipal(); + val principal = authentication.getPrincipal(); + if (!(principal instanceof UserDetails)) { + log.error("Invalid user principal. {}", principal); + return false; + } boolean hasPermission = false; - - if (userInfo instanceof UserDetails) { - UserPrincipal principal = (UserPrincipal) userInfo; - Long userId = principal.getId(); - - List rolesByUserId = roleService.getRolesByUserId(userId); - List roleIds = rolesByUserId.stream() - .map(RolePO::getId) - .collect(Collectors.toList()); - List permissionsByRoleId = permissionService.selectByRoleIdList(roleIds); - - // Filter button permission for frond-end - List btnPerms = - permissionsByRoleId.stream() - // Sieve out page permissions - .filter(permission -> Objects.equals(permission.getType(), - PermissionType.BUTTON.getType())) - // Sieve out permission that has no URL - .filter(permission -> StrUtil.isNotBlank(permission.getUrl())) - // Sieve out permission that has no method - .filter(permission -> StrUtil.isNotBlank(permission.getMethod())) - .collect(Collectors.toList()); - - for (PermissionPO btnPerm : btnPerms) { - AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod()); - if (antPathMatcher.matches(request)) { - hasPermission = true; - break; - } + UserPrincipal userPrincipal = (UserPrincipal) principal; + Long userId = userPrincipal.getId(); + // TODO: auth-center roleService.getRolesByUserId(userId) + List rolesByUserId = roleService.getRolesByUserId(userId); + val payload = new GetPermissionListByRoleIdListPayload(); + rolesByUserId.forEach(rolePO -> { + payload.getRoleIdList().add(rolePO.getId()); + }); + val permissionListByRoleIdListResponse = authCenterRemoteApi.getPermissionListByRoleIdList(payload); + val permissionList = permissionListByRoleIdListResponse.getData().getPermissionList(); + // Filter button permission for frond-end + List buttonPermissionList = + permissionList.stream() + // Sieve out page permissions + .filter(permission -> ObjectUtil.equal(permission.getType(), + PermissionType.BUTTON.getType())) + // Sieve out permission that has no URL + .filter(permission -> StrUtil.isNotBlank(permission.getUrl())) + // Sieve out permission that has no method + .filter(permission -> StrUtil.isNotBlank(permission.getMethod())) + .collect(Collectors.toList()); + for (GetPermissionListByRoleIdListResponse.Permission btnPerm : buttonPermissionList) { + // TODO: check is AntPathRequestMatcher supports RESTFul request + AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod()); + if (antPathMatcher.matches(request)) { + hasPermission = true; + break; } - - return hasPermission; - } else { - return false; } + return hasPermission; + } /** @@ -101,9 +101,8 @@ public boolean hasPermission(HttpServletRequest request, Authentication authenti * @param request HTTP Request */ private void checkRequest(HttpServletRequest request) { - String currentMethod = request.getMethod(); - Multimap urlMapping = allUrlMapping(); - + val currentMethod = request.getMethod(); + val urlMapping = allUrlMapping(); for (String uri : urlMapping.keySet()) { // 通过 AntPathRequestMatcher 匹配 url // 可以通过 2 种方式创建 AntPathRequestMatcher @@ -112,15 +111,13 @@ private void checkRequest(HttpServletRequest request) { // 2:new AntPathRequestMatcher(uri) 这种方式不校验请求方法,只校验请求路径 AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri); if (antPathMatcher.matches(request)) { - if (!urlMapping.get(uri) - .contains(currentMethod)) { + if (!urlMapping.get(uri).contains(currentMethod)) { throw new SecurityException(HttpStatus.METHOD_NOT_ALLOWED); } else { return; } } } - throw new SecurityException(HttpStatus.NOT_FOUND); } @@ -130,24 +127,19 @@ private void checkRequest(HttpServletRequest request) { * @return {@link ArrayListMultimap} 格式的 URL Mapping */ private Multimap allUrlMapping() { - Multimap urlMapping = ArrayListMultimap.create(); - + Multimap urlMapping = LinkedListMultimap.create(); // 获取url与类和方法的对应信息 - Map handlerMethods = mapping.getHandlerMethods(); - + val handlerMethods = mapping.getHandlerMethods(); handlerMethods.forEach((key, value) -> { // 获取当前 key 下的获取所有URL - Set url = key.getPatternsCondition() - .getPatterns(); + val url = key.getPatternsCondition().getPatterns(); RequestMethodsRequestCondition method = key.getMethodsCondition(); - // 为每个URL添加所有的请求方法 url.forEach(item -> urlMapping.putAll(item, method.getMethods() .stream() .map(Enum::toString) .collect(Collectors.toList()))); }); - return urlMapping; } } diff --git a/common/src/main/java/com/jmsoftware/common/util/ResponseUtil.java b/common/src/main/java/com/jmsoftware/common/util/ResponseUtil.java index 123769f3..a4fce5d8 100644 --- a/common/src/main/java/com/jmsoftware/common/util/ResponseUtil.java +++ b/common/src/main/java/com/jmsoftware/common/util/ResponseUtil.java @@ -5,6 +5,7 @@ import com.jmsoftware.common.constant.HttpStatus; import com.jmsoftware.common.exception.BaseException; import lombok.extern.slf4j.Slf4j; +import lombok.val; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -30,14 +31,21 @@ public class ResponseUtil { * @param data Data */ public static void renderJson(final HttpServletResponse response, final HttpStatus httpStatus, final Object data) { + standardizeHttpServletResponse(response, httpStatus); + val responseBodyBean = ResponseBodyBean.ofStatus(httpStatus.getCode(), + httpStatus.getMessage(), data); + try { + response.getWriter().write(MAPPER.writeValueAsString(responseBodyBean)); + } catch (IOException e) { + log.error("Error occurred when responding a data JSON.", e); + } + } + + public static void renderJson(final HttpServletResponse response, final HttpStatus httpStatus, + final String message) { + standardizeHttpServletResponse(response, httpStatus); + val responseBodyBean = ResponseBodyBean.ofStatus(httpStatus.getCode(), message, null); try { - ResponseBodyBean responseBodyBean = ResponseBodyBean.ofStatus(httpStatus.getCode(), - httpStatus.getMessage(), - data); - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "*"); - response.setContentType("application/json;charset=UTF-8"); - response.setStatus(httpStatus.getCode()); response.getWriter().write(MAPPER.writeValueAsString(responseBodyBean)); } catch (IOException e) { log.error("Error occurred when responding a data JSON.", e); @@ -51,17 +59,29 @@ public static void renderJson(final HttpServletResponse response, final HttpStat * @param exception Exception */ public static void renderJson(final HttpServletResponse response, final BaseException exception) { + val httpStatus = HttpStatus.fromCode(exception.getCode()); + standardizeHttpServletResponse(response, httpStatus); + val responseBodyBean = ResponseBodyBean.ofStatus(exception.getCode(), + exception.getMessage(), + exception.getData()); try { - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "*"); - response.setContentType("application/json;charset=UTF-8"); - response.setStatus(exception.getCode()); - ResponseBodyBean responseBodyBean = ResponseBodyBean.ofStatus(exception.getCode(), - exception.getMessage(), - exception.getData()); response.getWriter().write(MAPPER.writeValueAsString(responseBodyBean)); } catch (IOException e) { log.error("Error occurred when responding an exception JSON.", e); } } + + /** + * Standardize http servlet response. + * + * @param response the response + * @param httpStatus the http status + */ + private static void standardizeHttpServletResponse(final HttpServletResponse response, + final HttpStatus httpStatus) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "*"); + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(httpStatus.getCode()); + } }