网站的制作公司,网络营销师工作内容,window7用jsp做的网站要什么工具,网站内链规划文章目录 一、介绍二、auth微服务代码1. SecurityConfig2. UserDetailsService3. 总结 三、gateway微服务代码1. 统一处理CORS问题 四、content微服务代码1. controller2. SecurityConfig3. 解析JWT Utils4. 总结 五、一些坑 书接上文 微服务OAuth 2.1认证授权可行性方案(Sprin… 文章目录 一、介绍二、auth微服务代码1. SecurityConfig2. UserDetailsService3. 总结 三、gateway微服务代码1. 统一处理CORS问题 四、content微服务代码1. controller2. SecurityConfig3. 解析JWT Utils4. 总结 五、一些坑 书接上文 微服务OAuth 2.1认证授权可行性方案(Spring Security 6)
一、介绍
三个微服务
auth微服务作为认证服务器用于颁发JWT。gateway微服务作为网关用于拦截过滤。content微服务作为资源服务器用于校验授权。
以下是授权相关数据库。
user表示用户表role表示角色表user_role关联了用户和角色表示某个用户是是什么角色。一个用户可以有多个角色menu表示资源权限表。PreAuthorize(hasAuthority(xxx))时用的就是这里的code。permission关联了角色和资源权限表示某个角色用于哪些资源访问权限一个角色有多个资源访问权限。
当我们知道userId我们就可以知道这个用户可以访问哪些资源并把这些权限也就是menu里的code字段写成数组写到JWT的负载部分的authorities字段中。当用户携带此JWT访问具有PreAuthorize(hasAuthority(xxx))修饰的资源时我们解析出JWT中的authorities字段判断是否包含hasAuthority指定的xxx权限以此来完成所谓的的“授权”。
二、auth微服务代码
1. SecurityConfig
package com.xuecheng.auth.config;import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;/*** 身份验证服务器安全配置** author mumu* date 2024/02/13*/
//EnableGlobalMethodSecurity(securedEnabled true, prePostEnabled true)
Configuration
EnableWebSecurity
public class AuthServerSecurityConfig {private static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(RSA);keyPairGenerator.initialize(2048);keyPair keyPairGenerator.generateKeyPair();} catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}/*** 密码编码器* 用于加密认证服务器client密码和用户密码** return {link PasswordEncoder}*/Beanpublic PasswordEncoder passwordEncoder() {// 密码为明文方式// return NoOpPasswordEncoder.getInstance();// 或使用 BCryptPasswordEncoderreturn new BCryptPasswordEncoder();}/*** 授权服务器安全筛选器链* br/* 来自Spring Authorization Server示例用于暴露Oauth2.1端点一般不影响常规的请求** param http http* return {link SecurityFilterChain}* throws Exception 例外*/BeanOrder(1)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0http// Redirect to the login page when not authenticated from the// authorization endpoint.exceptionHandling((exceptions) - exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint(/login),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))// Accept access tokens for User Info and/or Client Registration.oauth2ResourceServer((resourceServer) - resourceServer.jwt(Customizer.withDefaults()));return http.build();}/*** 默认筛选器链* br/* 这个才是我们需要关心的过滤链可以指定哪些请求被放行哪些请求需要JWT验证** param http http* return {link SecurityFilterChain}* throws Exception 例外*/BeanOrder(2)public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -authorize.requestMatchers(new AntPathRequestMatcher(/actuator/**)).permitAll().requestMatchers(new AntPathRequestMatcher(/login)).permitAll().requestMatchers(new AntPathRequestMatcher(/logout)).permitAll().requestMatchers(new AntPathRequestMatcher(/wxLogin)).permitAll().requestMatchers(new AntPathRequestMatcher(/register)).permitAll().requestMatchers(new AntPathRequestMatcher(/oauth2/**)).permitAll().requestMatchers(new AntPathRequestMatcher(/**/*.html)).permitAll().requestMatchers(new AntPathRequestMatcher(/**/*.json)).permitAll().requestMatchers(new AntPathRequestMatcher(/auth/**)).permitAll().anyRequest().authenticated()).csrf(AbstractHttpConfigurer::disable)//指定logout端点用于退出登陆不然二次获取授权码时会自动登陆导致短时间内无法切换用户.logout(logout - logout.logoutUrl(/logout).addLogoutHandler(new SecurityContextLogoutHandler()).logoutSuccessUrl(http://www.51xuecheng.cn)).formLogin(Customizer.withDefaults()).oauth2ResourceServer(oauth2 - oauth2.jwt(Customizer.withDefaults())
// .jwt(jwt - jwt
// .jwtAuthenticationConverter(jwtAuthenticationConverter())
// ));return http.build();}private JwtAuthenticationConverter jwtAuthenticationConverter() {JwtAuthenticationConverter jwtConverter new JwtAuthenticationConverter();return jwtConverter;}/*** 客户端管理实例* br/* 来自Spring Authorization Server示例** return {link RegisteredClientRepository}*/Beanpublic RegisteredClientRepository registeredClientRepository() {RegisteredClient registeredClient RegisteredClient.withId(UUID.randomUUID().toString()).clientId(XcWebApp).clientSecret(passwordEncoder().encode(XcWebApp)).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).redirectUri(http://www.51xuecheng.cn).redirectUri(http://localhost:63070/auth/wxLogin).redirectUri(http://www.51xuecheng.cn/sign.html)
// .postLogoutRedirectUri(http://localhost:63070/login?logout).scope(all).scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).scope(message.read).scope(message.write).scope(read).scope(write).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(2)) // 设置访问令牌的有效期.refreshTokenTimeToLive(Duration.ofDays(3)) // 设置刷新令牌的有效期.reuseRefreshTokens(true) // 是否重用刷新令牌.build()).build();return new InMemoryRegisteredClientRepository(registeredClient);}/*** jwk源* br/* 对访问令牌进行签名的示例里面包含公私钥信息。** return {link JWKSource}{link SecurityContext}*/Beanpublic JWKSourceSecurityContext jwkSource() {KeyPair keyPair generateRsaKey();RSAPublicKey publicKey (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey (RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();JWKSet jwkSet new JWKSet(rsaKey);return new ImmutableJWKSet(jwkSet);}/*** jwt解码器* br/* JWT解码器主要就是基于公钥信息来解码** param jwkSource jwk源* return {link JwtDecoder}*/Beanpublic JwtDecoder jwtDecoder(JWKSourceSecurityContext jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}/*** JWT定制器* BR/* 可以往JWT从加入额外信息这里是加入authorities字段是一个权限数组。** return {link OAuth2TokenCustomizer}{link JwtEncodingContext}*/Beanpublic OAuth2TokenCustomizerJwtEncodingContext jwtTokenCustomizer() {return context - {Authentication authentication context.getPrincipal();if (authentication.getPrincipal() instanceof UserDetails userDetails) {ListString authorities userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());context.getClaims().claim(authorities, authorities);}};}
}这里需要注意几点
使用BCryptPasswordEncoder密码加密在设置clientSecret时需要手动使用密码编码器。jwtTokenCustomizer解析UserDetails然后往JWT中添加authorities字段为了后面的授权。
2. UserDetailsService
package com.xuecheng.ucenter.service.impl;import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.ucenter.mapper.XcMenuMapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.dto.AuthParamsDto;
import com.xuecheng.ucenter.model.dto.XcUserExt;
import com.xuecheng.ucenter.model.po.XcMenu;
import com.xuecheng.ucenter.model.po.XcUser;
import com.xuecheng.ucenter.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.userdetails.User;
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.Component;import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;Component
Slf4j
public class UserServiceImpl implements UserDetailsService {Autowiredprivate MyAuthService myAuthService;AutowiredXcMenuMapper xcMenuMapper;/*** 用户统一认证** param s 用户信息Json字符串* return {link UserDetails}* throws UsernameNotFoundException 找不到用户名异常*/Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {XcUserExt xcUserExt myAuthService.execute(username);return getUserPrincipal(xcUserExt);}public UserDetails getUserPrincipal(XcUserExt user){//用户权限,如果不加报Cannot pass a null GrantedAuthority collectionListXcMenu xcMenus xcMenuMapper.selectPermissionByUserId(user.getId());String[] permissions {read};if (ObjectUtils.isNotEmpty(xcMenus)){permissions xcMenus.stream().map(XcMenu::getCode).toList().toArray(String[]::new);log.info(权限如下:{}, Arrays.toString(permissions));}//为了安全在令牌中不放密码String password user.getPassword();user.setPassword(null);//将user对象转jsonString userString JSON.toJSONString(user);//创建UserDetails对象return User.withUsername(userString).password(password).authorities(permissions).build();}
}
这里需要注意几点
username就是前端/auth/login的时候输入的账户名。myAuthService.execute(username)不抛异常就默认表示账户存在此时将password加入UserDetails 并返回Spring Authorization Server对比校验两个密码。myAuthService.execute(username)根据username获取用户信息返回将用户信息存入withUsername中Spring Authorization Server默认会将其加入到JWT中。现在Spring Authorization Server默认不会把authorities(permissions)写入JWT需要配合OAuth2TokenCustomizer手动写入。
3. 总结
这样auth微服务颁发的JWT现在就会包含authorities字段。示例如下
{active: true,sub: {\cellphone\:\17266666637\,\createTime\:\2024-02-13 10:33:13\,\email\:\1138882663qq.com\,\id\:\012f3a90-2bc9-4a2c-82a3-f9777c9ac10a\,\name\:\xiamu\,\nickname\:\xiamu\,\permissions\:[],\status\:\1\,\updateTime\:\2024-02-13 10:33:13\,\username\:\xiamu\,\utype\:\101001\,\wxUnionid\:\test\},aud: [XcWebApp],nbf: 1707830437,scope: all,iss: http://localhost:63070/auth,exp: 1707837637,iat: 1707830437,jti: 8a657c60-968f-4d98-8a4c-22a7b4ecd333,authorities: [xc_sysmanager,xc_sysmanager_company,xc_sysmanager_doc,xc_sysmanager_log,xc_teachmanager_course_list],client_id: XcWebApp,token_type: Bearer
}三、gateway微服务代码
1. 统一处理CORS问题
EnableWebFluxSecurity
Configuration
public class SecurityConfig {//安全拦截配置Beanpublic SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {return http.cors(cors - cors.configurationSource(corsConfigurationSource())).authorizeExchange(exchanges -exchanges.pathMatchers(/**).permitAll().anyExchange().authenticated()).oauth2ResourceServer(oauth2 - oauth2.jwt(Customizer.withDefaults())).csrf(ServerHttpSecurity.CsrfSpec::disable).build();}Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration corsConfig new CorsConfiguration();corsConfig.addAllowedOriginPattern(*); // 允许任何源corsConfig.addAllowedMethod(*); // 允许任何HTTP方法corsConfig.addAllowedHeader(*); // 允许任何HTTP头corsConfig.setAllowCredentials(true); // 允许证书cookiescorsConfig.setMaxAge(3600L); // 预检请求的缓存时间秒UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration(/**, corsConfig); // 对所有路径应用这个配置return source;}
}
这里需要注意几点
书接上文这里虽然用了oauth2.jwt(Customizer.withDefaults())但实际上基于远程auth微服务开放的jwkSetEndpoint配置的JwtDecoder。.cors(cors - cors.configurationSource(corsConfigurationSource()))一次性处理CORS问题。
四、content微服务代码
1. controller PreAuthorize(hasAuthority(xc_teachmanager_course_list))ApiResponse(responseCode 200, description Successfully retrieved user)Operation(summary 查询课程信息列表)PostMapping(/course/list)public PageResultCourseBase list(PageParams pageParams,Parameter(description 请求具体内容) RequestBody(required false) QueryCourseParamsDto dto){SecurityUtil.XcUser xcUser SecurityUtil.getUser();if (xcUser ! null){System.out.println(xcUser.getUsername());System.out.println(xcUser.toString());}return courseBaseInfoService.queryCourseBaseList(pageParams, dto);}使用了PreAuthorize(hasAuthority(xc_teachmanager_course_list))修饰的controller资源。
2. SecurityConfig
Configuration
EnableWebSecurity
EnableMethodSecurity
public class SecurityConfig {//安全拦截配置Beanpublic SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -authorize.requestMatchers(/**).permitAll().anyRequest().authenticated()).csrf(AbstractHttpConfigurer::disable).oauth2ResourceServer(oauth - oauth.jwt(jwt - jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));return http.build();}private JwtAuthenticationConverter jwtAuthenticationConverter() {JwtAuthenticationConverter jwtConverter new JwtAuthenticationConverter();jwtConverter.setJwtGrantedAuthoritiesConverter(jwt - {// 从JWT的claims中提取权限信息ListString authorities jwt.getClaimAsStringList(authorities);if (authorities null) {return Collections.emptyList();}return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());});return jwtConverter;}
}需要注意几点
使用EnableMethodSecurity让PreAuthorize生效和gateway一样需要基于远程auth微服务开放的jwkSetEndpoint配置JwtDecoder。指定JwtAuthenticationConverter 让anyRequest().authenticated()需要验证的请求除了完成默认的JWT验证外还需要完成JwtAuthenticationConverter 指定逻辑。JwtAuthenticationConverter 中将JWT的authorities部分形成数组后写入GrantedAuthorities这正是spring security6用于校验PreAuthorize的字段。
3. 解析JWT Utils
Slf4j
public class SecurityUtil {public static XcUser getUser(){Authentication authentication SecurityContextHolder.getContext().getAuthentication();if (authentication null){return null;}if (authentication instanceof JwtAuthenticationToken) {JwtAuthenticationToken jwtAuth (JwtAuthenticationToken) authentication;System.out.println(jwtAuth);MapString, Object tokenAttributes jwtAuth.getTokenAttributes();System.out.println(tokenAttributes);Object sub tokenAttributes.get(sub);return JSON.parseObject(sub.toString(), XcUser.class);}return null;}Datapublic static class XcUser implements Serializable {private static final long serialVersionUID 1L;private String id;private String username;private String password;private String salt;private String name;private String nickname;private String wxUnionid;private String companyId;/*** 头像*/private String userpic;private String utype;private LocalDateTime birthday;private String sex;private String email;private String cellphone;private String qq;/*** 用户状态*/private String status;private LocalDateTime createTime;private LocalDateTime updateTime;}
}把JWT的信息解析回XcUser 相当于用户携带JWT访问后端后端可以根据JWT获取此用户的信息。当然你可以尽情的自定义扩展。
4. 总结
当用户携带JWT访问需要权限的资源时现在可以正常的校验权限了。
五、一些坑
写RegisteredClient时注册那么多redirectUri是因为debug了很久才发现获取授权码和获取JWT时redirect_uri参数需要一致。cors问题spring secuity6似乎会一开始直接默认拒绝cors导致跨域请求刚到gateway就寄了到不了content微服务即使content微服务配置了CORS的处理方案也无济于事。