diff --git a/mystyle-cloud-api/src/main/java/com/zhangmeng/api/service/oauth/AuthControllerApi.java b/mystyle-cloud-api/src/main/java/com/zhangmeng/api/service/oauth/AuthControllerApi.java new file mode 100644 index 0000000..e9e9b04 --- /dev/null +++ b/mystyle-cloud-api/src/main/java/com/zhangmeng/api/service/oauth/AuthControllerApi.java @@ -0,0 +1,16 @@ +package com.zhangmeng.api.service.oauth; + +import com.zhangmeng.model.entity.User; +import com.zhangmeng.model.vo.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +@Api(tags = "用户授权管理") +public interface AuthControllerApi { + + @ApiOperation("令牌解析") + public User parseToken(); + + @ApiOperation("用户授权") + public Result login(String username, String password); +} diff --git a/mystyle-cloud-model/pom.xml b/mystyle-cloud-model/pom.xml index 35836c9..ac1b1f8 100644 --- a/mystyle-cloud-model/pom.xml +++ b/mystyle-cloud-model/pom.xml @@ -89,5 +89,15 @@ com.alibaba fastjson + + + io.jsonwebtoken + jjwt + + + + org.springframework.boot + spring-boot-starter-security + \ No newline at end of file diff --git a/mystyle-cloud-model/src/main/java/com/zhangmeng/model/entity/OauthConfig.java b/mystyle-cloud-model/src/main/java/com/zhangmeng/model/entity/OauthConfig.java new file mode 100644 index 0000000..16587b9 --- /dev/null +++ b/mystyle-cloud-model/src/main/java/com/zhangmeng/model/entity/OauthConfig.java @@ -0,0 +1,34 @@ +package com.zhangmeng.model.entity; + +import com.zhangmeng.model.base.baseEntity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * @author zhengmeng + * @version 1.0 + * @date 2021年1月7日17:44:40 + */ +@NoArgsConstructor +@Data +@AllArgsConstructor +@Entity +@EqualsAndHashCode(callSuper = false) +@Table(name = "oauth_config") +public class OauthConfig extends BaseEntity { + + private Integer ttl; + + private String clientId; + + private String clientSecret; + + private String cookieDomain; + + private Integer cookieMaxAge; +} diff --git a/mystyle-cloud-oauth/pom.xml b/mystyle-cloud-oauth/pom.xml new file mode 100644 index 0000000..1f2075f --- /dev/null +++ b/mystyle-cloud-oauth/pom.xml @@ -0,0 +1,68 @@ + + + + mystyle-cloud-parent + com.zhangmeng + 1.0-SNAPSHOT + + 4.0.0 + + mystyle-cloud-oauth + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-amqp + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + + org.springframework.cloud + spring-cloud-sleuth-zipkin + + + + org.springframework.security + spring-security-data + + + + org.springframework.cloud + spring-cloud-starter-oauth2 + 2.2.4.RELEASE + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + com.zhangmeng + mystyle-cloud-api + 1.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/OauthApplication.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/OauthApplication.java new file mode 100644 index 0000000..e7c3554 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/OauthApplication.java @@ -0,0 +1,20 @@ +package com.zhangmeng.oauth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +/** + * @author zhangmeng + * @date 2021年11月3日17:00:00 + * @version 1.0 + */ +@SpringBootApplication +@EnableDiscoveryClient +@EnableFeignClients +public class OauthApplication { + public static void main(String[] args) { + SpringApplication.run(OauthApplication.class,args); + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/AuthorizationServerConfig.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/AuthorizationServerConfig.java new file mode 100644 index 0000000..039057f --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/AuthorizationServerConfig.java @@ -0,0 +1,150 @@ +package com.zhangmeng.oauth.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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.ClientDetailsService; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; + +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.security.KeyPair; + +/** + * @author 转身的背影在心底里沉沦 + * @date 2021年9月10日09:46:38 + * @version 1.0 + */ +@Configuration +@EnableAuthorizationServer +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + + //数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取 + @Autowired + private DataSource dataSource; + + //jwt令牌转换器 + @Autowired + private JwtAccessTokenConverter jwtAccessTokenConverter; + + //SpringSecurity 用户自定义授权认证类 + @Qualifier("userDetailsServiceImpl") + @Autowired + UserDetailsService userDetailsService; + + //授权认证管理器 + @Autowired + AuthenticationManager authenticationManager; + + //令牌持久化存储接口 + @Autowired + TokenStore tokenStore; + + @Resource(name = "keyProp") + private KeyProperties keyProperties; + + /*** + * 客户端信息配置 + * @param clients + * @throws Exception + */ + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + // 动态获取客户端信息 + clients.jdbc(dataSource) // 加载数据源 + .clients(clientDetails()); // 查询客户端数据 +// clients.inMemory() //基于内存 +// .withClient("mystyle-cloud") //客户端id +// .secret("mystyle-cloud") //秘钥 +// .redirectUris("http://localhost") //重定向地址 +// .accessTokenValiditySeconds(60) //访问令牌有效期 +// .refreshTokenValiditySeconds(60) //刷新令牌有效期 +// .authorizedGrantTypes( +// "authorization_code", //根据授权码生成令牌 +// "client_credentials", //客户端认证 +// "refresh_token", //刷新令牌 +// "password") //密码方式认证 +// .scopes("app"); //客户端范围,名称自定义,必填 + } + + /*** + * 授权服务器端点配置 + * @param endpoints + * @throws Exception + */ + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + endpoints.accessTokenConverter(jwtAccessTokenConverter) + .authenticationManager(authenticationManager)//认证管理器 + .tokenStore(tokenStore) //令牌存储 + .userDetailsService(userDetailsService); //用户信息service + } + + /*** + * 授权服务器的安全配置 + * @param oauthServer + * @throws Exception + */ + @Override + public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { + oauthServer.allowFormAuthenticationForClients() + .passwordEncoder(new BCryptPasswordEncoder()) + .tokenKeyAccess("permitAll()") + .checkTokenAccess("isAuthenticated()"); + } + + //读取密钥的配置 + @Bean("keyProp") + public KeyProperties keyProperties() { + return new KeyProperties(); + } + + + //客户端配置 + @Bean + public ClientDetailsService clientDetails() { + return new JdbcClientDetailsService(dataSource); + } + + @Bean + @Autowired + public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { + return new JwtTokenStore(jwtAccessTokenConverter); + } + + /**** + * JWT令牌转换器 + * @param customUserAuthenticationConverter + * @return + */ + @Bean + public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) { + JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); + KeyPair keyPair = new KeyStoreKeyFactory( + keyProperties.getKeyStore().getLocation(), //证书路径 + keyProperties.getKeyStore().getSecret().toCharArray()) //证书秘钥 + .getKeyPair( + keyProperties.getKeyStore().getAlias(), //证书别名 + keyProperties.getKeyStore().getPassword().toCharArray()); //证书密码 + converter.setKeyPair(keyPair); + //配置自定义的CustomUserAuthenticationConverter + DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter(); + accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter); + return converter; + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/CustomUserAuthenticationConverter.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/CustomUserAuthenticationConverter.java new file mode 100644 index 0000000..626fa77 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/CustomUserAuthenticationConverter.java @@ -0,0 +1,49 @@ +package com.zhangmeng.oauth.config; + +import com.zhangmeng.oauth.utils.UserJwt; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 自定义用户授权转换 + */ +@Component +public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter { + + @Qualifier("userDetailsServiceImpl") + @Autowired + UserDetailsService userDetailsService; + + @Override + public Map convertUserAuthentication(Authentication authentication) { + HashMap response = new LinkedHashMap<>(); + String name = authentication.getName(); + response.put("username", name); + + Object principal = authentication.getPrincipal(); + UserJwt userJwt = null; + if (principal instanceof UserJwt) { + userJwt = (UserJwt) principal; + } else { + //refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt + UserDetails userDetails = userDetailsService.loadUserByUsername(name); + userJwt = (UserJwt) userDetails; + } + response.put("name", userJwt.getName()); + response.put("id", userJwt.getId()); + if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { + response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities())); + } + return response; + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/FeignOauth2RequestInterceptor.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/FeignOauth2RequestInterceptor.java new file mode 100644 index 0000000..df63033 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/FeignOauth2RequestInterceptor.java @@ -0,0 +1,42 @@ +package com.zhangmeng.oauth.config; + +import com.zhangmeng.oauth.utils.JwtToken; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + +/** + * 请求拦截器 + */ +@Configuration +public class FeignOauth2RequestInterceptor implements RequestInterceptor { + @Override + public void apply(RequestTemplate requestTemplate) { + // 获取的全部请求信息 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null){ + HttpServletRequest request = attributes.getRequest(); + // 获取所有的请求头信息 + Enumeration headerNames = request.getHeaderNames(); + if (headerNames != null){ + while (headerNames.hasMoreElements()){ + // 获取请求头的key + String element = headerNames.nextElement(); + // 获取请求头的value + String value = request.getHeader(element); + // 将请求头信息放入到请求头 + requestTemplate.header(element,value); + } + } + } + // 获得带有权限的令牌(将令牌放入请求头中,用于登录到的时候携带权限信息) + String token = JwtToken.adminJwt(); + // 如果微服务之间相互调用,也需要将令牌放入请求头中, + requestTemplate.header("Authorization","bearer " + token); + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/UserDetailsServiceImpl.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/UserDetailsServiceImpl.java new file mode 100644 index 0000000..16116e8 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/UserDetailsServiceImpl.java @@ -0,0 +1,80 @@ +package com.zhangmeng.oauth.config; + +import com.zhangmeng.domain.admin.Permission; +import com.zhangmeng.feign.admin.AdminFeign; +import com.zhangmeng.oauth.utils.UserJwt; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 自定义授权认证类 + * + * @author 转身的背影在心底里沉沦 + * @date 2021年9月14日11:54:013 + * @version 1.0 + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + ClientDetailsService clientDetailsService; + + @Autowired + PasswordEncoder passwordEncoder; + + @Autowired + private AdminFeign adminFeign; + + /**** + * 自定义授权认证 + * @param username + * @return + * @throws UsernameNotFoundException + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + //取出身份,如果身份为空说明没有认证 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + //没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret + if(authentication==null){ + ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username); + if(clientDetails!=null){ + //秘钥 + String clientSecret = clientDetails.getClientSecret(); + //静态方式 + //return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList("")); + //数据库查找方式 + return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList("")); + } + } + + if (StringUtils.isEmpty(username)) { + return null; + } + + // 通过数据库去查询用户通过密码授权 + com.zhangmeng.domain.admin.User user = this.adminFeign.findByUserName(username); + + //根据用户查询权限列表 + List permissions = this.adminFeign.findByUserId(user.getId()); + Set collect = permissions.parallelStream().filter(permission -> !org.springframework.util.StringUtils.isEmpty(permission.getTitle())) + .map(permission -> new SimpleGrantedAuthority(permission.getTitle())).collect(Collectors.toSet()); + return new UserJwt(username,user.getPassword(),collect); + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/WebSecurityConfig.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/WebSecurityConfig.java new file mode 100644 index 0000000..9f3cbf9 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/config/WebSecurityConfig.java @@ -0,0 +1,94 @@ +package com.zhangmeng.oauth.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +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; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@EnableWebSecurity +@Order(-1) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + /*** + * 采用BCryptPasswordEncoder对密码进行编码 + * @return + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /*** + * 忽略安全拦截的URL + * @param web + * @throws Exception + */ + @Override + public void configure(WebSecurity web) throws Exception { + // 对静态资源放行 + web.ignoring().antMatchers( + "/user/login", + "/user/logout", + "/oauth/login" + ); + } + + /** + * 跨域支持 + * + * @return + */ + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedMethods("*"); + } + }; + } + + /*** + * 创建授权管理认证对象 + * @return + * @throws Exception + */ + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + + + /**** + * + * @param http + * @throws Exception + */ + @Override + public void configure(HttpSecurity http) throws Exception { + + http.csrf().disable() + .httpBasic() //启用Http基本身份验证 + .and() + .formLogin() //启用表单身份验证 + // 自定义登录请求(视图请求) + .loginPage("/oauth/login") + // springSecurity自带登录地址 + .loginProcessingUrl("/user/login") + .and() + .authorizeRequests() //限制基于Request请求访问 + .anyRequest() + .authenticated(); //其他请求都需要经过验证 + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/controller/AuthController.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/controller/AuthController.java new file mode 100644 index 0000000..b03c97e --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/controller/AuthController.java @@ -0,0 +1,111 @@ +package com.zhangmeng.oauth.controller; + +import com.alibaba.cloud.commons.lang.StringUtils; +import com.zhangmeng.api.service.oauth.AuthControllerApi; +import com.zhangmeng.model.base.baseController.BaseController; +import com.zhangmeng.model.entity.OauthConfig; +import com.zhangmeng.model.entity.User; +import com.zhangmeng.model.vo.Result; +import com.zhangmeng.model.vo.StatusCode; +import com.zhangmeng.oauth.dto.AuthToken; +import com.zhangmeng.oauth.feign.AdminFeign; +import com.zhangmeng.oauth.service.AuthService; +import com.zhangmeng.oauth.service.OauthConfigService; +import com.zhangmeng.oauth.utils.CookieUtil; +import com.zhangmeng.oauth.utils.TokenTools; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +@RestController +@RequestMapping(value = "/user") +public class AuthController extends BaseController implements AuthControllerApi { + + @Autowired + private AdminFeign adminFeign; + + @Autowired + private OauthConfigService oauthConfigService; + + @Autowired + private AuthService authService; + + @Autowired + PasswordEncoder passwordEncoder; + + @Autowired + private TokenTools tokenTools; + + @Override + @GetMapping("/oauth/parseToken")//解析 + public User parseToken() { + Map userInfo = this.tokenTools.getUserInfo(); + String username = userInfo.get("username"); + return this.adminFeign.findByUserName(username); + } + + @Override + @PostMapping("/login") + public Result login(String username, String password) { + if (StringUtils.isEmpty(username)) { + throw new RuntimeException("用户名不允许为空"); + } + if (StringUtils.isEmpty(password)) { + throw new RuntimeException("密码不允许为空"); + } + User user = this.adminFeign.findByUserName(username); + if (user == null){ + throw new UsernameNotFoundException("用户名错误"); + } + //校验密码是否正确,如果正确则申请令牌 + if (!this.passwordEncoder.matches(password,user.getPassword())) { + throw new RuntimeException("密码错误"); + } + OauthConfig oauthConfig = this.oauthConfig(); + AuthToken authToken = auth_login(username, password,oauthConfig); + //用户身份令牌 + String access_token = authToken.getAccessToken(); + //将令牌存储到cookie + saveCookie(access_token,oauthConfig); + return new Result(true, StatusCode.OK, "登录成功!",authToken); + } + + private AuthToken auth_login(String username, String password,OauthConfig oauthConfig){ + //客户端ID + String clientId = oauthConfig.getClientId(); + //秘钥 + String clientSecret = oauthConfig.getClientSecret(); + //申请令牌 + return this.authService.login(username, password, clientId, clientSecret); + } + + private OauthConfig oauthConfig(){ + OauthConfig oauthConfig = this.oauthConfigService.oauthConfig(); + if (oauthConfig == null) { + throw new RuntimeException("oauthConfig 配置读取错误"); + } + return oauthConfig; + } + + /*** + * 将令牌存储到cookie + * @param token + */ + private void saveCookie(String token,OauthConfig oauthConfig) { + //Cookie存储的域名 + String cookieDomain = oauthConfig.getCookieDomain(); + //Cookie生命周期 + int cookieMaxAge = oauthConfig.getCookieMaxAge(); + HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); + CookieUtil.addCookie(response, cookieDomain, "/", "Authorization", token, cookieMaxAge, false); + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dao/OauthConfigDao.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dao/OauthConfigDao.java new file mode 100644 index 0000000..b012673 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dao/OauthConfigDao.java @@ -0,0 +1,10 @@ +package com.zhangmeng.oauth.dao; + + +import com.zhangmeng.model.base.baseDao.AbstractBaseMapper; +import com.zhangmeng.model.entity.OauthConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OauthConfigDao extends AbstractBaseMapper { +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/AuthToken.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/AuthToken.java new file mode 100644 index 0000000..f003f65 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/AuthToken.java @@ -0,0 +1,18 @@ +package com.zhangmeng.oauth.dto; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class AuthToken implements Serializable { + + //令牌信息 + String accessToken; + + //刷新token(refresh_token) + String refreshToken; + + //jwt短令牌 + String jti; +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/JwtToken.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/JwtToken.java new file mode 100644 index 0000000..1dfa0d4 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/JwtToken.java @@ -0,0 +1,55 @@ +package com.zhangmeng.oauth.dto; + +import com.alibaba.fastjson.JSON; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.jwt.Jwt; +import org.springframework.security.jwt.JwtHelper; +import org.springframework.security.jwt.crypto.sign.RsaSigner; +import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; + +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateKey; +import java.util.HashMap; +import java.util.Map; + +/** + * @author 转身的背影在心底里沉沦 + * @date 2021年9月18日12:23:40 + * @version 1.0 + */ +public class JwtToken { + public static String adminJwt(){ + //证书文件路径 + String key_location="mystyle-cloud.jks"; + //秘钥库密码 + String key_password="mystyle-cloud"; + //秘钥密码 + String keypwd = "mystyle-cloud"; + //秘钥别名 + String alias = "mystyle-cloud"; + + //访问证书路径 + ClassPathResource resource = new ClassPathResource(key_location); + + //创建秘钥工厂 + KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray()); + + //读取秘钥对(公钥、私钥) + KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray()); + + //获取私钥 + RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate(); + + //定义Payload9(载荷,给生成的令牌加权限) + Map tokenMap = new HashMap<>(); + // key 就是解析后的authorities, value是权限 信息可以有多个 + tokenMap.put("authorities", new String[]{"admin"}); + + //生成Jwt令牌 + Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate)); + + //取出令牌 + return jwt.getEncoded(); + } +} \ No newline at end of file diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/UserJwt.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/UserJwt.java new file mode 100644 index 0000000..7019c7f --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/dto/UserJwt.java @@ -0,0 +1,31 @@ +package com.zhangmeng.oauth.dto; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.Collection; + +public class UserJwt extends User { + private String id; //用户ID + private String name; //用户名字 + + public UserJwt(String username, String password, Collection authorities) { + super(username, password, authorities); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/feign/AdminFeign.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/feign/AdminFeign.java new file mode 100644 index 0000000..1db15ea --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/feign/AdminFeign.java @@ -0,0 +1,12 @@ +package com.zhangmeng.oauth.feign; + +import com.zhangmeng.model.entity.User; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.RequestMapping; + +@FeignClient("") +public interface AdminFeign { + + @RequestMapping("") + User findByUserName(String username); +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/AuthService.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/AuthService.java new file mode 100644 index 0000000..37da596 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/AuthService.java @@ -0,0 +1,12 @@ +package com.zhangmeng.oauth.service; + + +import com.zhangmeng.oauth.dto.AuthToken; + +public interface AuthService { + + /*** + * 授权认证方法 + */ + AuthToken login(String username, String password, String clientId, String clientSecret); +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/OauthConfigService.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/OauthConfigService.java new file mode 100644 index 0000000..d53b41c --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/OauthConfigService.java @@ -0,0 +1,10 @@ +package com.zhangmeng.oauth.service; + + +import com.zhangmeng.model.base.baseService.BaseService; +import com.zhangmeng.model.entity.OauthConfig; + +public interface OauthConfigService extends BaseService { + + public OauthConfig oauthConfig(); +} \ No newline at end of file diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/impl/AuthServiceImpl.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..e0bf0b5 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/impl/AuthServiceImpl.java @@ -0,0 +1,130 @@ +package com.zhangmeng.oauth.service.impl; + +import com.zhangmeng.oauth.dto.AuthToken; +import com.zhangmeng.oauth.service.AuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Service; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.Map; + +@Service +public class AuthServiceImpl implements AuthService { + + @Autowired + private LoadBalancerClient loadBalancerClient; + + @Autowired + private RestTemplate restTemplate; + + /*** + * 授权认证方法 + * @param username + * @param password + * @param clientId + * @param clientSecret + * @return + */ + @Override + public AuthToken login(String username, String password, String clientId, String clientSecret) { + //申请令牌 + AuthToken authToken = applyToken(username,password,clientId, clientSecret); + if(authToken == null){ + throw new RuntimeException("申请令牌失败"); + } + return authToken; + } + + + /**** + * 认证方法 + * @param username:用户登录名字 + * @param password:用户密码 + * @param clientId:配置文件中的客户端ID + * @param clientSecret:配置文件中的秘钥 + * @return AuthToken + */ + private AuthToken applyToken(String username, String password, String clientId, String clientSecret) { + //选中认证服务的地址 + ServiceInstance serviceInstance = loadBalancerClient.choose("mystyle-user-oauth"); + if (serviceInstance == null) { + throw new RuntimeException("找不到对应的服务"); + } + //获取令牌的url + String path = serviceInstance.getUri().toString() + "/oauth/token"; + //定义body + MultiValueMap formData = new LinkedMultiValueMap<>(); + //授权方式 + formData.add("grant_type", "password"); + //账号 + formData.add("username", username); + //密码 + formData.add("password", password); + //定义头 + MultiValueMap header = new LinkedMultiValueMap<>(); + header.add("Authorization", httpbasic(clientId, clientSecret)); + //指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值 + restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + public void handleError(ClientHttpResponse response) throws IOException { + //当响应的值为400或401时候也要正常响应,不要抛出异常 + if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) { + super.handleError(response); + } + } + }); + Map map = null; + try { + //http请求spring security的申请令牌接口 + ResponseEntity mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity>(formData, header), Map.class); + //获取响应数据 + map = mapResponseEntity.getBody(); + } catch (RestClientException e) { + throw new RuntimeException(e); + } + if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) { + //jti是jwt令牌的唯一标识作为用户身份令牌 + throw new RuntimeException("创建令牌失败!"); + } + + //将响应数据封装成AuthToken对象 + AuthToken authToken = new AuthToken(); + //访问令牌(jwt) + String accessToken = (String) map.get("access_token"); + //刷新令牌(jwt) + String refreshToken = (String) map.get("refresh_token"); + //jti,作为用户的身份标识 + String jwtToken= (String) map.get("jti"); + authToken.setJti(jwtToken); + authToken.setAccessToken(accessToken); + authToken.setRefreshToken(refreshToken); + return authToken; + } + + + /*** + * base64编码 + * @param clientId + * @param clientSecret + * @return + */ + private String httpbasic(String clientId,String clientSecret){ + //将客户端id和客户端密码拼接,按“客户端id:客户端密码” + String string = clientId+":"+clientSecret; + //进行base64编码 + byte[] encode = Base64Utils.encode(string.getBytes()); + return "Basic "+new String(encode); + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/impl/OauthConfigServiceImpl.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/impl/OauthConfigServiceImpl.java new file mode 100644 index 0000000..bd2fb8d --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/service/impl/OauthConfigServiceImpl.java @@ -0,0 +1,25 @@ +package com.zhangmeng.oauth.service.impl; + +import com.zhangmeng.model.base.baseService.impl.AbstractBaseServiceImpl; +import com.zhangmeng.model.entity.OauthConfig; +import com.zhangmeng.oauth.service.OauthConfigService; +import org.springframework.stereotype.Service; +import tk.mybatis.mapper.entity.Condition; +import tk.mybatis.mapper.entity.Example; + +import java.util.List; + +@Service +public class OauthConfigServiceImpl extends AbstractBaseServiceImpl implements OauthConfigService { + + public OauthConfig oauthConfig(){ + Condition condition = new Condition(OauthConfig.class); + Example.Criteria criteria = condition.createCriteria(); + condition.setOrderByClause("addTime desc"); + List oauthConfigs = this.findByCondition(condition); + if (oauthConfigs.size() > 0 ){ + return oauthConfigs.get(0); + } + return null; + } +} diff --git a/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/utils/CookieUtil.java b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/utils/CookieUtil.java new file mode 100644 index 0000000..ac1c123 --- /dev/null +++ b/mystyle-cloud-oauth/src/main/java/com/zhangmeng/oauth/utils/CookieUtil.java @@ -0,0 +1,52 @@ +package com.zhangmeng.oauth.utils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +public class CookieUtil { + + /** + * 设置cookie + * + * @param response + * @param name cookie名字 + * @param value cookie值 + * @param maxAge cookie生命周期 以秒为单位 + */ + public static void addCookie(HttpServletResponse response, String domain, String path, String name, + String value, int maxAge, boolean httpOnly) { + Cookie cookie = new Cookie(name, value); + cookie.setDomain(domain); + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(httpOnly); + response.addCookie(cookie); + } + + /** + * 根据cookie名称读取cookie + * @param request + * @return map + */ + + public static Map readCookie(HttpServletRequest request, String ... cookieNames) { + Map cookieMap = new HashMap(); + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + String cookieName = cookie.getName(); + String cookieValue = cookie.getValue(); + for(int i=0;i getUserInfo(){ + //获取授权信息 + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails(); + //令牌解码 + return dcodeToken(details.getTokenValue()); + } + + /*** + * 读取令牌数据 + */ + public Map dcodeToken(String token){ + //校验Jwt + Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(getPubKey())); + //获取Jwt原始内容 + String claims = jwt.getClaims(); + return JSON.parseObject(claims,Map.class); + } + + /** + * 获取非对称加密公钥 Key + * @return 公钥 Key + */ + public String getPubKey() { + if(!StringUtils.isEmpty(publickey)){ + return publickey; + } + Resource resource = new ClassPathResource(PUBLIC_KEY); + try { + InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); + BufferedReader br = new BufferedReader(inputStreamReader); + publickey = br.lines().collect(Collectors.joining("\n")); + return publickey; + } catch (IOException ioe) { + return null; + } + } +} diff --git a/mystyle-cloud-oauth/src/main/resources/application.yml b/mystyle-cloud-oauth/src/main/resources/application.yml new file mode 100644 index 0000000..2a80f3a --- /dev/null +++ b/mystyle-cloud-oauth/src/main/resources/application.yml @@ -0,0 +1,43 @@ +server: + port: 31006 +spring: + application: + name: mystyle-cloud-file + datasource: + username: root + password: root + url: jdbc:mysql://127.0.0.1:3306/mystyle-blog?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + database: mysql + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL5Dialect + zipkin: + sender: + type: web + base-url: http://localhost:9411/ + service: + name: mystyle-cloud-file + sleuth: + sampler: + probability: 1 + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 +mybatis: + type-aliases-package: com.zhangmeng.model.entity + configuration: + mapUnderscoreToCamelCase: true + default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler +mapper: + style: normal + enum-as-simple-type: true + identity: MYSQL + check-example-entity-class: true \ No newline at end of file diff --git a/mystyle-cloud-oauth/src/main/resources/mystyle-cloud.jks b/mystyle-cloud-oauth/src/main/resources/mystyle-cloud.jks new file mode 100644 index 0000000..3dcee18 Binary files /dev/null and b/mystyle-cloud-oauth/src/main/resources/mystyle-cloud.jks differ diff --git a/mystyle-cloud-oauth/src/main/resources/public.key b/mystyle-cloud-oauth/src/main/resources/public.key new file mode 100644 index 0000000..cfceccb --- /dev/null +++ b/mystyle-cloud-oauth/src/main/resources/public.key @@ -0,0 +1 @@ +-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAizuj0fBV2+dj4lM3G6efKYvC2czd07BqmzV++E2yBguVks3XWvsW8qlzmG+t1XBCnRFDI/t1Ddc/Jsnlfy4YzRN8otb/Xn6Yz9ACFvZIPGx/q0cqcrgVaR9rSQiSzsGTgUGHNJk8r3A4w9PSSB552Z9s6p5TsWK5ezlfgg+2ANKn1eJ6R/hzajS/B1bTAqYcl9ddo7prneoeAN5LjlMhc2e0cSVgQt8ALP+4x/bTMnDkMjG6R8lnDAxE27B2ZPaLOIOjkUMK+9mZa4RNBoCDG6J/fwPD1NUoVRCbyr/TVaS4EzyhfNK1QW3BlZ0NLSI/SFD3eryKaFQdacJHS31neQIDAQAB-----END PUBLIC KEY----- \ No newline at end of file diff --git a/pom.xml b/pom.xml index fff4a66..9eb3331 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ mystyle-cloud-api mystyle-cloud-canal mystyle-cloud-mq + mystyle-cloud-oauth @@ -53,6 +54,7 @@ 4.1.2-1.5.2 4.2.1-1.5.2 1.2.75 + 0.9.1 @@ -198,7 +200,11 @@ fastjson ${fastjson.version} - + + io.jsonwebtoken + jjwt + ${jwt.version} + \ No newline at end of file