<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最開始是一個單體應用,所有功能模組都寫在一個專案裡,後來覺得專案越來越大,於是決定把一些功能拆分出去,形成一個一個獨立的微服務,於是就有個問題了,登入、退出、許可權控制這些東西怎麼辦呢?總不能每個服務都複製一套吧,最好的方式是將認證與鑑權也單獨抽離出來作為公共的服務,業務系統只專心做業務介面開發即可,完全不用理會許可權這些與之不相關的東西了。於是,便有了下面的架構圖:
下面重點看一下統一認證中心和業務閘道器的建設
這裡採用 Spring Security + Spring Security OAuth2OAuth2是一種認證授權的協定,是一種開放的標準。最長用到的是授權碼模式和密碼模式,在本例中,用這兩種模式都可以。首先,引入相關依賴最主要的依賴是spring-cloud-starter-oauth2 ,引入它就夠了
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.5.RELEASE</version> </dependency>
這裡Spring Boot的版本是2.6.3
完整的pom如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.tgf</groupId> <artifactId>tgf-service-parent</artifactId> <version>1.3.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.soa.supervision.uaa</groupId> <artifactId>soas-uaa</artifactId> <version>0.0.1-SNAPSHOT</version> <name>soas-uaa</name> <properties> <java.version>1.8</java.version> <spring-cloud.version>2021.0.0</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.5.RELEASE</version> </dependency> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.19</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.mybatis.scripting</groupId> <artifactId>mybatis-freemarker</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
在授權伺服器中,主要是設定如何生成Token,以及註冊的使用者端有哪些
package com.soa.supervision.uaa.config; import com.soa.supervision.uaa.constant.AuthConstants; import com.soa.supervision.uaa.domain.SecurityUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.endpoint.TokenKeyEndpoint; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import javax.annotation.Resource; import javax.sql.DataSource; import java.security.KeyPair; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 授權伺服器設定 * 1、設定使用者端 * 2、設定Access_Token生成 * * @Author ChengJianSheng * @Date 2022/2/14 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Resource private DataSource dataSource; @Autowired private AuthenticationManager authenticationManager; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(new JdbcClientDetailsService(dataSource)); } public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); // security.tokenKeyAccess("permitAll()"); public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { List<TokenEnhancer> tokenEnhancerList = new ArrayList<>(); tokenEnhancerList.add(jwtTokenEnhancer()); tokenEnhancerList.add(jwtAccessTokenConverter()); TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(tokenEnhancerList); endpoints.accessTokenConverter(jwtAccessTokenConverter()) .tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager); /** * Token增強 */ public TokenEnhancer jwtTokenEnhancer() { return new TokenEnhancer() { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Map<String, Object> additionalInformation = new HashMap<>(); additionalInformation.put(AuthConstants.JWT_USER_ID_KEY, securityUser.getUserId()); additionalInformation.put(AuthConstants.JWT_USER_NAME_KEY, securityUser.getUsername()); additionalInformation.put(AuthConstants.JWT_DEPT_ID_KEY, securityUser.getDeptId()); ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(additionalInformation); return accessToken; } }; * 採用RSA加密演演算法對JWT進行簽名 public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; * 金鑰對 @Bean public KeyPair keyPair() { KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); public TokenKeyEndpoint tokenKeyEndpoint() { return new TokenKeyEndpoint(jwtAccessTokenConverter()); }
說明:
使用者端表結構如下:
DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '使用者端ID', `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '使用者端金鑰', `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '授權型別', `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `access_token_validity` int(11) NULL DEFAULT NULL COMMENT 'access_token的有效時間', `refresh_token_validity` int(11) NULL DEFAULT NULL COMMENT 'refresh_token的有效時間', `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否允許自動授權', PRIMARY KEY (`client_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; INSERT INTO `oauth_client_details` VALUES ('hello', 'order-resource', '$2a$10$1Vun/h63tI4C48BqLsy2Zel5q5M2VW6w8KThoMfxww49wf9uv/dKy', 'all', 'authorization_code,password,refresh_token', 'http://www.baidu.com', NULL, 7200, 7260, NULL, 'true'); INSERT INTO `oauth_client_details` VALUES ('sso-client-1', NULL, '$2a$10$CxEwmODmsp/HOB7LloeBJeqUjotmNzjpk2WmjxtPxAeOYifQWLfhW', 'all', 'authorization_code', 'http://localhost:9001/sso-client-1/login/oauth2/code/custom', NULL, 180, 240, NULL, 'true');
本例中採用RSA非對稱加密,金鑰檔案用的是java自帶的keytools生成的
將來,認證伺服器用私鑰對token加密,然後將公鑰公開
package com.soa.supervision.uaa.controller; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.security.KeyPair; import java.security.interfaces.RSAPublicKey; import java.util.Map; /** * @Author ChengJianSheng * @Date 2022/2/15 */ @RestController public class KeyPairController { @Autowired private KeyPair keyPair; @GetMapping("/rsa/publicKey") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
在WebSecurity中主要是設定使用者,以及哪些請求需要認證以後才能存取
package com.soa.supervision.uaa.config; import com.soa.supervision.uaa.service.impl.UserDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @Author ChengJianSheng * @Date 2022/2/14 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/rsa/publicKey", "/menu/tree").permitAll() .anyRequest().authenticated() .and().formLogin().permitAll() .and() .csrf().disable(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
UserDetailsService實現類
package com.soa.supervision.uaa.service.impl; import com.soa.supervision.uaa.domain.AuthUserDTO; import com.soa.supervision.uaa.domain.SecurityUser; import com.soa.supervision.uaa.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.Set; import java.util.stream.Collectors; /** * @Author ChengJianSheng * @Date 2022/2/14 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { AuthUserDTO authUserDTO = sysUserService.getAuthUserByUsername(username); if (null == authUserDTO) { throw new UsernameNotFoundException("使用者不存在"); } if (!authUserDTO.isEnabled()) { throw new LockedException("賬號被禁用"); Set<SimpleGrantedAuthority> authorities = authUserDTO.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet()); return new SecurityUser(authUserDTO.getUserId(), authUserDTO.getDeptId(), authUserDTO.getUsername(), authUserDTO.getPassword(), authUserDTO.isEnabled(), authorities); } }
SysUserService
package com.soa.supervision.uaa.service; import com.soa.supervision.uaa.domain.AuthUserDTO; import com.soa.supervision.uaa.entity.SysUser; import com.baomidou.mybatisplus.extension.service.IService; /** * <p> * 使用者表 服務類 * </p> * * @author ChengJianSheng * @since 2022-02-14 */ public interface SysUserService extends IService<SysUser> { AuthUserDTO getAuthUserByUsername(String username); }
AuthUserDTO
package com.soa.supervision.uaa.domain; import lombok.Data; import java.io.Serializable; import java.util.List; /** * @Author ChengJianSheng * @Date 2022/2/15 */ @Data public class AuthUserDTO implements Serializable { private Integer userId; private String username; private String password; private Integer deptId; private boolean enabled; private List<String> roles; }
SysUserServiceImpl
package com.soa.supervision.uaa.service.impl; import com.soa.supervision.uaa.domain.AuthUserDTO; import com.soa.supervision.uaa.entity.SysUser; import com.soa.supervision.uaa.mapper.SysUserMapper; import com.soa.supervision.uaa.service.SysUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * <p> * 使用者表 服務實現類 * </p> * * @author ChengJianSheng * @since 2022-02-14 */ @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Autowired private SysUserMapper sysUserMapper; @Override public AuthUserDTO getAuthUserByUsername(String username) { return sysUserMapper.selectAuthUserByUsername(username); } }
SysUserMapper
package com.soa.supervision.uaa.mapper; import com.soa.supervision.uaa.domain.AuthUserDTO; import com.soa.supervision.uaa.entity.SysUser; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * 使用者表 Mapper 介面 * * @author ChengJianSheng * @since 2022-02-14 */ public interface SysUserMapper extends BaseMapper<SysUser> { AuthUserDTO selectAuthUserByUsername(String username); }
SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.soa.supervision.uaa.mapper.SysUserMapper"> <resultMap id="authUserResultMap" type="com.soa.supervision.uaa.domain.AuthUserDTO"> <id property="userId" column="id"/> <result property="username" column="username"/> <result property="password" column="password"/> <result property="deptId" column="dept_id"/> <result property="enabled" column="enabled"/> <collection property="roles" ofType="string" javaType="list"> <result column="role_code"/> </collection> </resultMap> <!-- 根據使用者名稱查使用者 --> <select id="selectAuthUserByUsername" resultMap="authUserResultMap"> SELECT t1.id, t1.username, t1.`password`, t1.dept_id, t1.enabled, t3.`code` AS role_code FROM sys_user t1 LEFT JOIN sys_user_role t2 ON t1.id = t2.user_id LEFT JOIN sys_role t3 ON t2.role_id = t3.id WHERE t1.username = #{username} </select> </mapper>
UserDetails
package com.soa.supervision.uaa.domain; import lombok.AllArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Set; /** * @Author ChengJianSheng * @Date 2022/2/14 */ @AllArgsConstructor public class SecurityUser implements UserDetails { /** * 擴充套件欄位 */ private Integer userId; private Integer deptId; private String username; private String password; private boolean enabled; private Set<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } public String getPassword() { return password; public String getUsername() { return username; public boolean isAccountNonExpired() { return true; public boolean isAccountNonLocked() { public boolean isCredentialsNonExpired() { public boolean isEnabled() { return enabled; public Integer getUserId() { return userId; public Integer getDeptId() { return deptId; }
預設的登入url是/login,本例中沒有自定義登入頁面,而是使用預設的登入頁面
正常的密碼模式下,輸入使用者名稱和密碼,登入成功以後返回token。本例中使用密碼模式,所以寫了個登入介面,而且也是取巧,覆蓋了預設的/oauth/token端點
package com.soa.supervision.uaa.controller; import com.tgf.common.domain.RespResult; import com.tgf.common.util.RespUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import java.security.Principal; import java.util.HashMap; import java.util.Map; /** * @Author ChengJianSheng * @Date 2022/2/18 */ @RestController @RequestMapping("/oauth") public class AuthorizationController { @Autowired private TokenEndpoint tokenEndpoint; /** * 密碼模式 登入 * @param principal * @param parameters * @return * @throws HttpRequestMethodNotSupportedException */ @PostMapping("/token") public RespResult postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); Map<String, Object> map = new HashMap<>(); // 快取 return RespUtils.success(); } * 退出 @PostMapping("/logout") public RespResult logout() { // JSONObject payload = JwtUtils.getJwtPayload(); // String jti = payload.getStr(SecurityConstants.JWT_JTI); // JWT唯一標識 // Long expireTime = payload.getLong(SecurityConstants.JWT_EXP); // JWT過期時間戳(單位:秒) // if (expireTime != null) { // long currentTime = System.currentTimeMillis() / 1000;// 當前時間(單位:秒) // if (expireTime > currentTime) { // token未過期,新增至快取作為黑名單限制存取,快取時間為token過期剩餘時間 // redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (expireTime - currentTime), TimeUnit.SECONDS); // } // } else { // token 永不過期則永久加入黑名單 // redisTemplate.opsForValue().set(SecurityConstants.TOKEN_BLACKLIST_PREFIX + jti, null); // } // return Result.success("登出成功"); }
補充:授權碼模式獲取access_token
登入以後,前端會查詢選單並展示,下面是選單相關介面
SysMenuController
package com.soa.supervision.uaa.controller; import com.soa.supervision.uaa.domain.MenuVO; import com.soa.supervision.uaa.service.SysMenuService; import com.tgf.common.domain.RespResult; import com.tgf.common.util.RespUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Arrays; import java.util.List; /** * <p> * 選單表 前端控制器 * </p> * * @author ChengJianSheng * @since 2022-02-21 */ @RestController @RequestMapping("/menu") public class SysMenuController { @Autowired private SysMenuService sysMenuService; @GetMapping("/tree") public RespResult tree(String systemCode) { List<Integer> roleIds = Arrays.asList(1,2); List<MenuVO> voList = sysMenuService.getMenuByUserRoles(systemCode, roleIds); return RespUtils.success(voList); } }
SysMenuService
package com.soa.supervision.uaa.service; import com.soa.supervision.uaa.domain.MenuVO; import com.soa.supervision.uaa.entity.SysMenu; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * <p> * 選單表 服務類 * </p> * * @author ChengJianSheng * @since 2022-02-21 */ public interface SysMenuService extends IService<SysMenu> { List<MenuVO> getMenuByUserRoles(String systemCode, List<Integer> roleIds); }
SysMenuServiceImpl
package com.soa.supervision.uaa.service.impl; import com.soa.supervision.uaa.domain.MenuVO; import com.soa.supervision.uaa.entity.SysMenu; import com.soa.supervision.uaa.mapper.SysMenuMapper; import com.soa.supervision.uaa.service.SysMenuService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * <p> * 選單表 服務實現類 * </p> * * @author ChengJianSheng * @since 2022-02-21 */ @Service public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService { @Autowired private SysMenuMapper sysMenuMapper; /** * 構造選單樹 * @param systemCode * @param roleIds * @return */ @Override public List<MenuVO> getMenuByUserRoles(String systemCode, List<Integer> roleIds) { List<MenuVO> voList = new ArrayList<>(); List<SysMenu> sysMenuList = sysMenuMapper.selectMenuByRole(systemCode, roleIds); if (null == sysMenuList || sysMenuList.size() == 0) { return voList; } List<MenuVO> menuVOList = sysMenuList.stream().map(e->{ MenuVO vo = new MenuVO(); BeanUtils.copyProperties(e, vo); vo.setChildren(new ArrayList<>()); return vo; }).distinct().collect(Collectors.toList()); for (int i = 0; i < menuVOList.size(); i++) { for (int j = 0; j < menuVOList.size(); j++) { if (menuVOList.get(i).getId().equals(menuVOList.get(j).getId())) { continue; } if (menuVOList.get(i).getId().equals(menuVOList.get(j).getParentId())) { menuVOList.get(i).getChildren().add(menuVOList.get(j)); } return menuVOList.stream().filter(e->0==e.getParentId()).collect(Collectors.toList()); } }
MenuVO
package com.soa.supervision.uaa.domain; import lombok.Data; import java.io.Serializable; import java.util.List; /** * @Author ChengJianSheng * @Date 2022/2/21 */ @Data public class MenuVO implements Serializable { private Integer id; /** * 選單名稱 */ private String name; * 父級選單ID private Integer parentId; * 路由地址 private String routePath; * 元件 private String component; * 圖示 private String icon; * 排序號 private Integer sort; * 子選單 private List<MenuVO> children; }
SysMenuMapper
package com.soa.supervision.uaa.mapper; import com.soa.supervision.uaa.entity.SysMenu; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import java.util.List; /** * <p> * 選單表 Mapper 介面 * </p> * * @author ChengJianSheng * @since 2022-02-21 */ public interface SysMenuMapper extends BaseMapper<SysMenu> { List<SysMenu> selectMenuByRole(@Param("systemCode") String systemCode, @Param("roleIds") List<Integer> roleIds); }
SysMenuMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.soa.supervision.uaa.mapper.SysMenuMapper"> <!-- 根據角色查選單 --> <select id="selectMenuByRole" resultType="com.soa.supervision.uaa.entity.SysMenu"> SELECT t1.* FROM sys_menu t1 LEFT JOIN sys_role_menu t2 ON t1.id = t2.menu_id WHERE t1.system_code = #{systemCode} AND t1.hidden = 0 AND t2.role_id IN <foreach collection="roleIds" item="roleId" open="(" close=")" separator=",">#{roleId}</foreach> ORDER BY t1.sort ASC </select> </mapper>
application.yml
server: port: 8094 servlet: context-path: /soas-uaa spring: application: name: soas-uaa datasource: url: jdbc:mysql://192.168.28.22:3306/demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 1234567 redis: host: 192.168.28.01 port: 6379 password: 123456 logging: level: org: springframework: security: debug mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3閘道器
在這裡,閘道器相當於OAuth2中的資源伺服器這麼個角色。閘道器代理了所有的業務微服務,如果說那些業務服務是資源的,那麼閘道器就是資源的集合,存取閘道器就是存取資源,存取資源就要先認證再授權才能存取。同時,閘道器又相當於一個公共方法,因此在這裡做鑑權是比較合適的。
首先是依賴
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.tgf</groupId> <artifactId>tgf-service-parent</artifactId> <version>1.3.1-SNAPSHOT</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.soa.supervision.gateway</groupId> <artifactId>soas-gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>soas-gateway</name> <properties> <java.version>1.8</java.version> <spring-security.version>5.6.1</spring-security.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring-security.version}</version> <artifactId>spring-security-oauth2-resource-server</artifactId> <artifactId>spring-security-oauth2-jose</artifactId> <!-- spring-security-oauth2-jose的依賴中包含了nimbus-jose-jwt,只是版本不是最新的而已,這裡如果想使用更高版本的nimbus-jose-jwt的話可以重新宣告一下 --> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.15.2</version> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.21</version> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
application.yml
server: port: 8090 spring: cloud: gateway: routes: - id: soas-enterprise uri: http://127.0.0.1:8093 predicates: - Path=/soas-enterprise/** - id: soas-portal uri: http://127.0.0.1:8092 - Path=/soas-portal/** - id: soas-finance uri: http://127.0.0.1:8095 - Path=/soas-finance/** discovery: locator: enabled: false redis: host: 192.168.28.01 port: 6379 password: 123456 database: 9 security: oauth2: resourceserver: jwt: jwk-set-uri: http://localhost:8094/soas-uaa/rsa/publicKey secure: ignore: urls: - /soas-portal/auth/**
直接放行的url
package com.soa.supervision.gateway.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @Author ChengJianSheng * @Date 2021/12/15 */ @Data @Component @ConfigurationProperties(prefix = "secure.ignore") public class IgnoreUrlProperties { private String[] urls; }
logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="30 seconds" debug="false"> <property name="log.charset" value="utf-8" /> <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" /> <property name="log.dir" value="./logs" /> <!--輸出到控制檯--> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${log.pattern}</pattern> <charset>${log.charset}</charset> </encoder> </appender> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.dir}/soas-gateway.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.dir}/soas-gateway.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <root level="info"> <appender-ref ref="console" /> <appender-ref ref="file" /> </root> </configuration>
真正的許可權判斷或者說許可權控制是在這裡,下面這段程式碼尤為重要,而且它在整個閘道器過濾器之前呼叫
package com.soa.supervision.gateway.config; import com.alibaba.fastjson.JSON; import com.soa.supervision.gateway.constant.AuthConstants; import com.soa.supervision.gateway.constant.RedisConstants; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.server.authorization.AuthorizationContext; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @Author ChengJianSheng * @Date 2022/2/16 */ @Slf4j @Component public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> { private final PathMatcher pathMatcher = new AntPathMatcher(); @Autowired private StringRedisTemplate stringRedisTemplate; @Override public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) { ServerHttpRequest request = context.getExchange().getRequest(); String path = request.getURI().getPath(); // token不能為空且有效 String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER); if (StringUtils.isBlank(token) || !token.startsWith(AuthConstants.JWT_TOKEN_PREFIX)) { return Mono.just(new AuthorizationDecision(false)); } String realToken = token.trim().substring(7); Long ttl = stringRedisTemplate.getExpire(RedisConstants.ONLINE_TOKEN_PREFIX_KV + realToken); if (ttl <= 0) { // 獲取存取資源所需的角色 List<String> authorizedRoles = new ArrayList<>(); // 擁有存取許可權的角色 Map<Object, Object> urlRoleMap = stringRedisTemplate.opsForHash().entries(RedisConstants.URL_ROLE_MAP_HK); for (Map.Entry<Object, Object> entry : urlRoleMap.entrySet()) { String permissionUrl = (String) entry.getKey(); List<String> roles = JSON.parseArray((String) entry.getValue(), String.class); if (pathMatcher.match(permissionUrl, path)) { authorizedRoles.addAll(roles); } // 沒有設定許可權規則表示無需授權,直接放行 if (CollectionUtils.isEmpty(authorizedRoles)) { return Mono.just(new AuthorizationDecision(true)); // 判斷使用者擁有的角色是否可以存取資源 return authentication.filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) .map(GrantedAuthority::getAuthority).any(authorizedRoles::contains) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); } }
選單許可權在Redis中是這樣儲存的
url -> [角色編碼, 角色編碼, 角色編碼]
查詢SQL
SELECT t1.url, t3.`code` AS role_code FROM sys_menu t1 LEFT JOIN sys_role_menu t2 ON t1.id = t2.menu_id LEFT JOIN sys_role t3 ON t2.role_id = t3.id WHERE t1.url is NOT NULL;
儲存到Redis
HSET "/soas-order/order/pageList" "["admin","org"]" HSET "/soas-order/order/save" "["admin","enterprise"]"
ResourceServerConfig
package com.soa.supervision.gateway.config; import cn.hutool.core.codec.Base64; import cn.hutool.core.io.IoUtil; import com.soa.supervision.gateway.util.ResponseUtils; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.io.InputStream; import java.security.KeyFactory; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; /** * @Author ChengJianSheng * @Date 2022/02/15 */ @Configuration @EnableWebFluxSecurity public class ResourceServerConfig { @Autowired private IgnoreUrlProperties ignoreUrlProperties; private AuthorizationManager authorizationManager; @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { // 設定JWT解碼相關 http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());//.publicKey(rsaPublicKey()); http.authorizeExchange() .pathMatchers(ignoreUrlProperties.getUrls()).permitAll() .anyExchange().access(authorizationManager) .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler()) .authenticationEntryPoint(authenticationEntryPoint()) .csrf().disable(); return http.build(); } public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); /** * 未授權(沒有存取許可權) */ public ServerAccessDeniedHandler accessDeniedHandler() { return (ServerWebExchange exchange, AccessDeniedException denied) -> { Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(resp -> ResponseUtils.writeErrorInfo(resp, HttpStatus.UNAUTHORIZED)); return mono; }; * 未登入 public ServerAuthenticationEntryPoint authenticationEntryPoint() { return (ServerWebExchange exchange, AuthenticationException ex) -> { Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(resp -> ResponseUtils.writeErrorInfo(resp, HttpStatus.FORBIDDEN)); * 測試本地公鑰(可選) @SneakyThrows public RSAPublicKey rsaPublicKey() { Resource resource = new ClassPathResource("public.key"); InputStream is = resource.getInputStream(); String publicKeyData = IoUtil.read(is).toString(); X509EncodedKeySpec keySpec = new X509EncodedKeySpec((Base64.decode(publicKeyData))); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); RSAPublicKey rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec); return rsaPublicKey; }
說明:
公鑰可以從遠端獲取,也可以放在本地從本地讀取。上面程式碼中,被註釋調的就是測試一下從本地讀取公鑰。
從原始碼中我們也可以看出有多種方式,本例中採用的是從遠端獲取,因此在前面application.yml中設定了spring.security.oauth2.resourceserver.jwt.jwk-set-uri
響應工具類ResponseUtils
package com.soa.supervision.gateway.util; import com.alibaba.fastjson.JSON; import com.tgf.common.domain.RespResult; import com.tgf.common.util.RespUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; /** * @Author ChengJianSheng * @Date 2022/2/16 */ public class ResponseUtils { public static Mono<Void> writeErrorInfo(ServerHttpResponse response, HttpStatus httpStatus) { response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getHeaders().set("Access-Control-Allow-Origin", "*"); response.getHeaders().set("Cache-Control", "no-cache"); RespResult respResult = RespUtils.fail(httpStatus.value(), httpStatus.getReasonPhrase()); String body = JSON.toJSONString(respResult); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)) .doOnError(error -> DataBufferUtils.release(buffer)); } }
鑑權通過以後,可以解析token,並將一些有用的資訊放到header中傳給下游的業務服務,這樣的話業務服務就無需再解析token了,在閘道器這裡統一處理是最適合的了
TokenFilter
package com.soa.supervision.gateway.filter; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.nimbusds.jose.JWSObject; import com.soa.supervision.gateway.constant.AuthConstants; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.text.ParseException; /** * 只有當請求URL匹配路由規則時才會執行全域性過濾器 * * @Author ChengJianSheng * @Date 2021/12/15 */ @Slf4j @Component public class TokenFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER); if (StringUtils.isBlank(token)) { return chain.filter(exchange); } String realToken = token.trim().substring(7); try { JWSObject jwsObject = JWSObject.parse(realToken); String payload = jwsObject.getPayload().toString(); JSONObject jsonObject = JSON.parseObject(payload); String userId = jsonObject.getString("userId"); String deptId = jsonObject.getString("deptId"); request = request.mutate() .header(AuthConstants.HEADER_USER_ID, userId) .header(AuthConstants.HEADER_DEPT_ID, deptId) .build(); // 可以把整個Payload放到請求頭中 // exchange.getRequest().mutate().header("user", payload).build(); exchange = exchange.mutate().request(request).build(); } catch (ParseException e) { log.error("解析token失敗!原因: {}", e.getMessage(), e); return chain.filter(exchange); } }
最後,是幾個常數類
AuthConstants
package com.soa.supervision.gateway.constant; /** * @Author ChengJianSheng * @Date 2021/11/17 */ public class AuthConstants { public static final String ROLE_PREFIX = "ROLE_"; public static final String JWT_TOKEN_HEADER = "Authorization"; public static final String JWT_TOKEN_PREFIX = "Bearer "; public static final String TOKEN_WHITELIST_PREFIX = "TOKEN:"; public static final String HEADER_USER_ID = "x-user-id"; public static final String HEADER_DEPT_ID = "x-dept-id"; }
RedisConstants
package com.soa.supervision.gateway.constant; /** * @Author ChengJianSheng * @Date 2022/2/16 */ public class RedisConstants { // 資源角色對映關係 public static final String URL_ROLE_MAP_HK = "URL_ROLE_HS"; // 有效的TOKEN public static final String ONLINE_TOKEN_PREFIX_KV = "ONLINE_TOKEN:"; }
最後,資料庫指令碼
DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `system_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '系統名稱', `system_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '系統編碼', `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '選單名稱', `parent_id` int(11) NOT NULL COMMENT '父級選單ID', `route_path` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由地址', `component` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '元件', `icon` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '圖示', `sort` smallint(8) NOT NULL COMMENT '排序號', `hidden` tinyint(4) NOT NULL COMMENT '是否隱藏(1:是,0:否)', `create_time` datetime NOT NULL COMMENT '建立時間', `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間', `create_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '建立人', `update_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '修改人', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '選單表' ROW_FORMAT = DYNAMIC; DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `menu_id` int(11) NOT NULL COMMENT '選單ID', `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名稱', `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'URL', `create_time` datetime NULL DEFAULT NULL COMMENT '建立時間', `update_time` datetime NULL DEFAULT NULL COMMENT '修改時間', ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '許可權表' ROW_FORMAT = Dynamic; DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名稱', `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色編碼', `update_time` datetime NOT NULL COMMENT '修改時間', `create_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '建立人', `update_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '修改人', ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC; DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `role_id` int(11) NOT NULL COMMENT '角色ID', ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色選單表' ROW_FORMAT = DYNAMIC;
專案截圖
https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/
https://docs.spring.io/spring-security/reference/index.html
https://github.com/spring-projects/spring-security-samples/tree/5.6.x
https://github.com/spring-projects/spring-security/wiki
到此這篇關於Spring Security實現統一登入與許可權控制的範例程式碼的文章就介紹到這了,更多相關Spring Security登入許可權控制內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45