赞
踩
传统登陆认证介绍
单点登陆认证介绍
OAuth2简介
OAuth2角色
OAuth2协议流程介绍
OAuth2授权类型
OAuth2授权码模式流程
OAuth2简化模式
OAuth2密码模式
OAuth2客户端模式
Spring Security OAuth设计
传统登陆方式是在每个服务进行登陆认证, 每个服务保存自己的用户数据, 并独立实现登陆认证逻辑。
随着服务的不断扩展, 用户数据很难集中统一,开发成本不断增加, 用户交互也极为不便 。
单点登陆是通过统一认证授权服务, 完成所有服务节点的登陆授权工作。
只需一台认证服务器,统一用户数据库, 完成用户认证授权, 控制资源访问, 支持其他服务或第三方应用接入, 扩展性强, 开发和运维成本降低。
OAuth 2.0 是一个行业的标准授权协议。OAuth 2.0 专注于简化客户端开发人员,同时为 Web 应用程序,桌面应用程序,手机等各种设备接入提供特定的授权流程。
OAuth2 实质是为第三方应用颁发一个具有时效性的Token令牌,使其他服务或第三方应用能够通过令牌获取相关资源。 常见的场景: 比如进入某个网站没有账号信息, 但可以通过QQ、微信、支付宝等账号进行登陆, 在这个登陆过程中采用的就是Oauth2协议; 对外API服务接口, 也一般采用OAUTH2授权, 比如微信API、新浪API等。
参考官方文档: https://oauth.net/2/
Resource Owner 与 Client 之间 , 资源所有者向Client发起认证请求, Client再返回认证授权信息。
Client 收到 Resource Owner 的认证请求后, 会去Authorization Server 申请访问令牌, Authorization Server会让Client 进行认证, 通过之后会返回Access Token。
Client 拿到 Authorization Server 的 Acceess Token , 访问Resource Server,Resource Server 验证之后, 返回被保护的资源信息。
Resource Server 可以通过JWT在本地进行验证, 也可以访问 Authorization Server, 对Client 的请求的合法性进行验证。
OAuth2 分为四种授权类型, 分别为:
资源拥有者(用户)通过代理(WEB浏览器)访问客户端程序,发起授权码模式认证。
客户端(Client,比如CSDN论坛)向认证服务器(Auth Server,QQ账号认证服务)发起请求, 此时客户端携带了客户端标识(client_id, 标识来源是CSDN)和重定向地址(redirect_uri, 一般是CSDN的地址)。
用户确认授权,客户端(Client)接收到code。
在重定向的过程中,客户端拿到 code 与 client_id
、client_secret
去授权服务器请求令牌,整个过程,用户代理是不会拿到令牌 token 的。
客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了, 比如获取QQ基本资料, 头像等信息。
授权请求:
- response_type=code // 必选项
- &client_id={客户端的ID} // 必选项
- &redirect_uri={重定向URI} // 可选项
- &scope={申请的权限范围} // 可选项
- &state={任意值} // 可选项
授权响应参数:
- code={授权码} // 必填
- &state={任意文字} // 如果授权请求中包含 state的话那就是必填
令牌请求:
- grant_type=authorization_code // 必填
- &code={授权码} // 必填 必须是认证服务器响应给的授权码
- &redirect_uri={重定向URI} // 如果授权请求中包含 redirect_uri 那就是必填
- &code_verifier={验证码} // 如果授权请求中包含 code_challenge 那就是必填
令牌响应:
- "access_token":"{访问令牌}", // 必填
- "token_type":"{令牌类型}", // 必填
- "expires_in":{过期时间}, // 任意
- "refresh_token":"{刷新令牌}", // 任意
- "scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填
-
-
- +----------+
- | Resource |
- | Owner |
- | |
- +----------+
- ^
- |
- (B)
- +----|-----+ Client Identifier +---------------+
- | -+----(A)-- & Redirection URI --->| |
- | User- | | Authorization |
- | Agent -|----(B)-- User authenticates -->| Server |
- | | | |
- | |<---(C)--- Redirection URI ----<| |
- | | with Access Token +---------------+
- | | in Fragment
- | | +---------------+
- | |----(D)--- Redirection URI ---->| Web-Hosted |
- | | without Fragment | Client |
- | | | Resource |
- | (F) |<---(E)------- Script ---------<| |
- | | +---------------+
- +-|--------+
- | |
- (A) (G) Access Token
- | |
- ^ v
- +---------+
- | |
- | Client |
- | |
- +---------+
-
授权请求:
- response_type=token // 必选项
- &client_id={客户端的ID} // 必选项
- &redirect_uri={重定向URI} // 可选项
- &scope={申请的权限范围} // 可选项
- &state={任意值} // 可选项
授权响应参数:
- &access_token={令牌信息} // 必填
- &expires_in={过期时间} // 任意
- &state={任意文字} // 如果授权请求中包含 state 那就是必填
- &scope={授权范围} // 如果请求和响应的授权范围不一致就必填
问题:为什么要有授权码和简化模式?看完这两种模式, 可能会有些疑问, 为什么要这么麻烦, 直接一次请求返回TOKEN不就可以吗?
我们可以看出, 两者主要差别, 是少了code验证环节, 直接返回token了, code验证是客户端与认证服务器在后台进行请求获取, 代理是获取不到TOKEN的, 如果缺少这个环节, 直接返回TOKEN, 相当于直接暴露给所有参与者, 存在安全隐患, 所以简化模式,一般用于信赖度较高的环境中使用。
- +----------+
- | Resource |
- | Owner |
- | |
- +----------+
- v
- | Resource Owner
- (A) Password Credentials
- |
- v
- +---------+ +---------------+
- | |>--(B)---- Resource Owner ------->| |
- | | Password Credentials | Authorization |
- | Client | | Server |
- | |<--(C)---- Access Token ---------<| |
- | | (w/ Optional Refresh Token) | |
- +---------+ +---------------+
-
令牌请求:
- grant_type=password // 必填
- &username={用户ID} // 必填
- &password={密码} // 必填
- &scope={授权范围} // 任意
令牌响应:
- "access_token":"{访问令牌}", // 必填
- "token_type":"{令牌类型}", // 必填
- "expires_in":"{过期时间}", // 任意
- "refresh_token":"{刷新令牌}", // 任意
- "scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填
-
此模式简化相关步骤, 直接通过用户和密码等隐私信息进行请求认证, 认证服务器直接返回token, 这需要整个环境具有较高的安全性。
- +---------+ +---------------+
- | | | |
- | |>--(A)- Client Authentication --->| Authorization |
- | Client | | Server |
- | |<--(B)---- Access Token ---------<| |
- | | | |
- +---------+ +---------------+
这种模式一般在内部服务之间应用, 授权一次, 长期可用, 不用刷新token。
令牌请求:
- grant_type=client_credentials // 必填
- client_id={客户端的ID} // 必填
- client_secret={客户端的密钥} // 必填
- &scope={授权范围} // 任意
令牌响应:
- "access_token":"{访问令牌}", // 必填
- "token_type":"{令牌类型}", // 必填
- "expires_in":"{过期时间}", // 任意
- "scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填
-
Spring Security OAuth2 的整体设计, 我们会在项目中集成Spring Security 组件实现OAuth2统一授权认证。
参考:
理解OAuth2: https://www.kancloud.cn/kancloud/oauth_2_0/63331
图解授权模式: https://learnku.com/articles/20082
整体设计:
IDEA 环境,安装 lombok插件
安装REDIS及图形化工具
安装Nacos
安装MySQL数据库
安装PostMan
创建公用组件服务, 为了便于微服务之间公用功能的复用, 减少不必要的重复工作, 同时统一组件管理
结构:
统一实体工程:
服务说明:
统一认证服务实现OAUTH2认证功能。服务设计上, 采用的是增强token方式,这套流程会包含token扩展, 缓存集成认证, 密码模式, 加密处理, 自动化配置等功能, 学会这套流程的使用, 能够利用Spring Security处理大部分业务场景,包括jwt的实现。
这里数据层采用的是JPA实现, 自定义用户服务接口,实现用户密码模式认证。
工程结构:
工程依赖
父级工程依赖属性配置:
- <properties>
- ...
- <spring-cloud-starter.version>2.1.1.RELEASE</spring-cloud-starter.version>
- <druid.version>1.1.18</druid.version>
- <project.stock.version>1.0.0</project.stock.version>
- </properties>
POM.XML文件:
- <dependencies>
- <!-- OATUH2 的核心组件 -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-oauth2</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
-
- <!-- Spring Security 依赖 -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-security</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
-
- <!-- 自动化缓存依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
-
- <!--缓存依赖-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
-
- <!-- jpa -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
-
- <!-- Nacos服务注册发现依赖 -->
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
- </dependency>
-
-
- <!-- 公用utils工具的依赖 -->
- <dependency>
- <groupId>com.itcast.trade</groupId>
- <artifactId>bulls-stock-common-utils</artifactId>
- <version>${project.stock.version}</version>
- </dependency>
-
- <!-- 公用实体的依赖 -->
- <dependency>
- <groupId>com.itcast.trade</groupId>
- <artifactId>bulls-stock-entity</artifactId>
- <version>${project.stock.version}</version>
- </dependency>
-
- <!-- druid 连接池依赖 -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>${druid.version}</version>
- </dependency>
-
- <!-- 解决druid log4j监控组件无法使用问题, 不用再额外增加其他LOG框架 -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </dependency>
-
- <!-- mysql-connector-java -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <version>8.0.14</version>
- </dependency>
-
- <!--freemarker-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-freemarker</artifactId>
- </dependency>
- <!--web 模块-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
-
- <!--tomcat容器, javax servelet相关依赖-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-tomcat</artifactId>
- </dependency>
-
- </dependencies>
-
- <!-- 打包配置 -->
- <build>
- <finalName>trade-auth</finalName>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-resources-plugin</artifactId>
- <executions>
- <execution>
- <id>default-resources</id>
- <phase>validate</phase>
- <goals>
- <goal>copy-resources</goal>
- </goals>
- <configuration>
- <outputDirectory>target/classes</outputDirectory>
- <useDefaultDelimiters>false</useDefaultDelimiters>
- <delimiters>
- <delimiter>${*}</delimiter>
- </delimiters>
- <resources>
- <resource>
- <directory>src/main/resources/</directory>
- <filtering>true</filtering>
- </resource>
- <resource>
- <directory>src/main/java</directory>
- <includes>
- <include>**/*.xml</include>
- </includes>
- <filtering>false</filtering>
- </resource>
- </resources>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>repackage</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
-
- </build>
启动类(TradeAuthApplication)
- @SpringBootApplication
- @EnableDiscoveryClient
- @ComponentScan(basePackages = {"com.itcast"})
- @EntityScan(basePackages = {"com.itcast"})
- @EnableJpaRepositories(basePackages = {"com.itcast"})
- @EnableCaching
- public class TradeAuthApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(TradeAuthApplication.class, args);
- }
- }
-
使用JPA功能, 需要开启@EntityScan与@EnableJpaRepositories两个注解, 扫描指定路径。
@EnableCaching是启用了Spring Cache缓存功能, 缓存是基于redis实现。
实体
去实体工程里面, 创建用户信息实体TradeUser:
- @Data
- @Entity
- @Table(name = "t_trade_user")
- public class TradeUser extends BaseEntity {
-
- @Id
- private Long id;
-
- /**
- * 用户编号
- */
- private String userNo;
-
- /**
- * 用户名称
- */
- private String name;
-
- /**
- * 用户密码
- */
- private String userPwd;
-
- /**
- * 电话号码
- */
- private String phone;
-
- /**
- * 公司ID
- */
- private Long companyId;
-
- /**
- * 邮箱
- */
- private String email;
-
- /**
- * 地址
- */
- private String address;
-
- /**
- * 最近一次用户登陆IP
- */
- private String lastLoginIp;
-
- /**
- * 最近一次登陆时间
- */
- private Date lastLoginTime;
-
- /**
- * 状态(0:有效, 1:锁定, 2:禁用)
- */
- private int status;
-
- /**
- * 创建时间
- */
- private Date craeteTime;
-
-
- }
JPA实体要开启Entity,Table, ID三个标注。我们采用自动转换处理, 不用再加Column注解。
数据层
用户信息数据接口TradeUserRepository
- /**
- * 用户信息数据层接口
- */
- @Repository("tradeUserRepository")
- public interface TradeUserRepository extends PagingAndSortingRepository<TradeUser, String>, JpaSpecificationExecutor<TradeUser> {
-
- /**
- * 根据用户账号获取用户对象
- * @param userNo
- * @return
- */
- public TradeUser findByUserNo(String userNo);
- }
我们通过JPA, 实现一个根据用户账号获取用户对象接口, 用于用户登陆处理。注意路径与实体TradeUser的路径要在上面讲的JPA扫描路径范围之内。
服务层
用户信息服务接口AuthStockUserDetailServiceImpl
-
- @Service("authStockUserDetailService")
- public class AuthStockUserDetailServiceImpl implements UserDetailsService {
-
- @Autowired
- private TradeUserRepository tradeUserRepository;
-
- @Autowired
- private CacheManager cacheManager;
-
- @Override
- public UserDetails loadUserByUsername(String userNo) throws UsernameNotFoundException {
-
- // 查询缓存
- Cache cache = cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS);
- if (cache != null && cache.get(userNo) != null) {
- return (UserDetails) cache.get(userNo).get();
- }
- // 缓存未找到, 查询数据库
- TradeUser tradeUser = tradeUserRepository.findByUserNo(userNo);;
- if(null == tradeUser){
- throw new UsernameNotFoundException(userNo + " not valid !");
- }
- // 封装成OAUTH鉴权的用户对象
- UserDetails userDetails = new OAuthTradeUser(tradeUser);
- // 将用户信息放入缓存
- cache.put(userNo, userDetails);
- return userDetails;
- }
- }
-
-
这是Spring Security 提供的用户信息接口, 采用OAUTH的密码模式, 需要实现该接口的loadUserByUsername方法,为提升性能, 这里我们加入了Spring Cache缓存处理。
OAuthTradeUser用户封装信息:
- public class OAuthTradeUser extends User {
-
- private static final long serialVersionUUID = -1L;
-
- /**
- * 业务用户信息
- */
- private TradeUser tradeUser;
-
- public OAuthTradeUser(TradeUser tradeUser) {
- // OAUTH2认证用户信息构造处理
- super(tradeUser.getUserNo(), tradeUser.getUserPwd(), (tradeUser.getStatus() == 0 ? true : false),
- true, true, (tradeUser.getStatus() == 0 ? true : false), Collections.emptyList());
- this.tradeUser = tradeUser;
- }
-
- }
-
客户端信息服务接口
- public class AuthClientDetailService extends JdbcClientDetailsService {
-
- public AuthClientDetailService(DataSource dataSource) {
- super(dataSource);
- }
-
- /**
- * 重写原生方法支持redis缓存
- *
- * @param clientId
- * @return
- * @throws InvalidClientException
- */
- @Override
- @Cacheable(value = GlobalConstants.OAUTH_KEY_CLIENT_DETAILS, key = "#clientId", unless = "#result == null")
- public ClientDetails loadClientByClientId(String clientId) {
- return super.loadClientByClientId(clientId);
- }
-
- }
-
这是OAUTH内置的客户端信息, 重新它是为了实现缓存, 减少数据库查询。 对应的表为t_oauth_client_details。因为走的是redis缓存来处理鉴权, 其他OAUTH的内置表可以不用加入。
编写Web接口
TradeStockTokenController, Token信息接口:
- @RestController
- @RequestMapping("/token")
- @Log4j2
- public class TradeStockTokenController {
-
- private static final String STOCK_OAUTH_ACCESS = GlobalConstants.OAUTH_PREFIX_KEY;
-
- @Autowired
- private RedisTemplate stockRedisTemplate;
-
- @Autowired
- private TokenStore tokenStore;
-
- @Autowired
- private CacheManager cacheManager;
-
- /**
- * 认证页面
- *
- * @return ModelAndView
- */
- @RequestMapping("/login")
- public ModelAndView require() {
- return new ModelAndView("ftl/login");
- }
-
-
- /**
- * 认证页面
- *
- * @return ModelAndView
- */
- @RequestMapping("/success")
- public String success() {
- log.info("token login success!");
- return "login success";
- }
-
-
- /**
- * 退出token
- *
- * @param authHeader Authorization
- */
- @DeleteMapping("/logout")
- public String logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
- if (StringUtils.isEmpty(authHeader)) {
- return "退出失败,token 为空";
- }
-
- String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();
- OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
- if (accessToken == null || StringUtils.isEmpty(accessToken.getValue())) {
- return "退出失败,token 无效";
- }
-
- OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
- cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS).evict(auth2Authentication.getName());
- tokenStore.removeAccessToken(accessToken);
- return "退出成功, token 已清除";
- }
-
- /**
- * 令牌管理调用
- *
- * @param token token
- * @return
- */
- @DeleteMapping("/{token}")
- public String delToken(@PathVariable("token") String token) {
- OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
- tokenStore.removeAccessToken(oAuth2AccessToken);
- return "token 已清除";
- }
- }
TradeUserController用户接口:
- @RestController
- @RequestMapping("/trade")
- public class TradeUserController {
-
- @Autowired
- private UserDetailsService authStockUserDetailService;
-
- /**
- * 获取用户信息
- * @param username
- * @return
- */
- @RequestMapping("/user")
- @ResponseBody
- public UserDetails getUser(@RequestParam("username")String username) {
-
- UserDetails userDetails = authStockUserDetailService.loadUserByUsername(username);
-
- return userDetails;
- }
- }
-
配置类
- /**
- * Redis 缓存配置
- * @return
- */
- @Bean
- public RedisTemplate<String, Object> stockRedisTemplate() {
- RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
- redisTemplate.setKeySerializer(new StringRedisSerializer());
- redisTemplate.setHashKeySerializer(new StringRedisSerializer());
- redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
- redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- return redisTemplate;
- }
-
-
- /**
- * 自定义Client查询,可以修改表名, 字段等
- * @param clients
- */
- @Override
- @SneakyThrows
- public void configure(ClientDetailsServiceConfigurer clients) {
- AuthClientDetailService clientDetailsService = new AuthClientDetailService(dataSource);
- clientDetailsService.setSelectClientDetailsSql(DEFAULT_SELECT_STATEMENT);
- clientDetailsService.setFindClientDetailsSql(DEFAULT_FIND_STATEMENT);
- clients.withClientDetails(clientDetailsService);
- }
-
-
- /**
- * t_oauth_client_details 表的字段,不包括client_id、client_secret
- */
- String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "
- + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
- + "refresh_token_validity, additional_information, autoapprove";
-
- /**
- * JdbcClientDetailsService 查询语句
- */
- String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS
- + " from t_oauth_client_details";
-
- /**
- * 默认的查询语句
- */
- String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";
-
- /**
- * 按条件client_id 查询
- */
- String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
-
-
- /**
- * 防止申请token时出现401错误
- * @param oauthServer
- */
- @Override
- public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
- oauthServer
- .tokenKeyAccess("permitAll()")
- .checkTokenAccess("permitAll()")
- .allowFormAuthenticationForClients();
- }
-
- /**
- * 认证服务配置
- * @param endpoints
- */
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
- endpoints
- .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
- .tokenStore(tokenStore())
- .tokenEnhancer(tokenEnhancer())
- .userDetailsService(authStockUserDetailService)
- .authenticationManager(authenticationManager)
- .reuseRefreshTokens(false);
- }
-
-
- }
这里包含了认证服务的配置, TokenStore实现配置, Token增强配置以及自定义Client的查询实现。
注意要加上@EnableAuthorizationServer注解。
Web服务认证配置WebSecurityConfigurer
这里我们实现了一个获取用户信息接口,以及自定义login登陆处理, 用于OAUTH的验证。
- @Primary
- @Order(90)
- @Configuration
- public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
-
- @Autowired
- private UserDetailsService authStockUserDetailService;
-
- @Autowired
- private StockPasswordEncoder stockPasswordEncoder;
-
- /**
- * Web服务认证配置
- * @param http
- */
- @Override
- @SneakyThrows
- protected void configure(HttpSecurity http) {
- http
- .formLogin()
- .loginPage("/token/login")
- .loginProcessingUrl("/token/form")
- .defaultSuccessUrl("/token/success")
- .and()
- .authorizeRequests()
- .antMatchers(
- "/token/**",
- "/actuator/**",
- "/druid/**").permitAll()
- .anyRequest().authenticated()
- .and().csrf().disable();
- }
-
/**
* 不拦截静态资源
@param web
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(“/css/**”);
}
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() {
return super.authenticationManagerBean();
}
@Autowired
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authStockUserDetailService).passwordEncoder(stockPasswordEncoder);
}
}
-
- + 密码加密处理器StockPasswordEncoder
-
- 使用密码加密器后, OAuth内置的Client认证以及用户密码认证都会加密处理。
-
- ```java
- @Component
- @Log4j2
- public class StockPasswordEncoder implements PasswordEncoder {
-
- /**
- * 编码处理
- * @param rawPassword
- * @return
- */
- @Override
- public String encode(CharSequence rawPassword) {
- return rawPassword.toString();
- }
-
- /**
- * 密码校验判断
- * @param rawPassword
- * @param encodedPassword
- * @return
- */
- @Override
- public boolean matches(CharSequence rawPassword, String encodedPassword) {
- if(rawPassword != null && rawPassword.length() > 0){
- try {
- // 这里通过MD5及B64加密
- String password = EncryptUtil.encryptSigned(rawPassword.toString());
- boolean isMatch= encodedPassword.equals(password);
- if(!isMatch) {
- log.warn("password 不一致!");
- }
- return isMatch;
- } catch (ComponentException e) {
- log.error(e.getMessage(), e);
- }
- }
- return false;
- }
-
- }
-
EncryptUtils加密类的实现
- public class EncryptUtil {
-
- private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
-
-
- private final static char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
- 'e', 'f' };
- public static String BASE64Encrypt;
-
- public final static String MD5ToString(String signed) {
-
- char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
- try {
-
- byte[] res = signed.getBytes("UTF-8");
- MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
- mdTemp.update(res);
- byte[] md = mdTemp.digest();
-
- // 把密文转换成十六进制的字符串形式
- int j = md.length;
- char str[] = new char[j * 2];
- int k = 0;
- for (int i = 0; i < j; i++) {
- byte byte0 = md[i];
- str[k++] = hexDigits[byte0 >>> 4 & 0xf];
- str[k++] = hexDigits[byte0 & 0xf];
- }
- return new String(str);
- } catch (Exception e) {
- logger.error(e.getMessage(), e);
- return null;
- }
- }
-
- public final static byte[] MD5(String str) {
- try {
- byte[] res = str.getBytes("UTF-8");
- MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
- mdTemp.update(res);
- byte[] hash = mdTemp.digest();
- return hash;
- } catch (Exception e) {
- return null;
- }
- }
-
- /**
- * MD5值计算<p>
- * MD5的算法在RFC1321 中定义:
- * 在RFC 1321中,给出了Test suite用来检验你的实现是否正确:
- * MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
- * MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
- * MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
- * MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
- * MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
- *
- * @param res 源字符串
- * @return md5值
- */
- public final static byte[] MD5EncrtyReutrnhexDigitsByteArray(String str) {
- try {
- byte[] res = str.getBytes("UTF-8");
- MessageDigest mdTemp = MessageDigest.getInstance("MD5".toUpperCase());
- mdTemp.update(res);
- byte[] hash = mdTemp.digest();
- return hash;
- } catch (Exception e) {
- return null;
- }
- }
-
- public final static String MD5EncrtyReturnString(String str) {
-
- byte[] b = MD5EncrtyReutrnhexDigitsByteArray(str);
-
- StringBuffer resultSb = new StringBuffer();
- for (int i = 0; i < b.length; i++) {
- int n = b[i];
- if (n < 0)
- n = 256 + n;
- int d1 = n / 16;
- int d2 = n % 16;
- resultSb.append(hexDigits[d1]);
- resultSb.append(hexDigits[d2]);
- }
- return resultSb.toString();
-
- }
-
- // 加密后解密
- public static String JM(byte[] inStr) {
- String newStr = new String(inStr);
- char[] a = newStr.toCharArray();
- for (int i = 0; i < a.length; i++) {
- a[i] = (char) (a[i] ^ 't');
- }
- String k = new String(a);
- return k;
- }
-
- /**
- * BASE64加密MD5EncrtyReutrnhexDigitsByteArray
- *
- * @param key
- * @return
- * @throws Exception
- */
- public static String BASE64Encrypt(byte[] key) throws ComponentException {
- String edata = null;
- try {
- edata = (new BASE64Encoder()).encodeBuffer(key).trim();
- } catch (Exception e) {
- throw new ComponentException(e.getMessage() +
- "BASE64编码错误!key=" + new String(key) + ", error=" + e.getMessage());
- }
- return edata;
- }
-
- /**
- * BASE64解密
- *
- * @param key
- * @return
- * @throws Exception
- */
- public static byte[] BASE64Decrypt(String data) {
- if (data == null)
- return null;
- byte[] edata = null;
- try {
- edata = (new BASE64Decoder()).decodeBuffer(data);
- return edata;
- } catch (Exception e) {
- logger.error(e.getMessage(), e);
- }
- return null;
- }
-
-
- /**
- * 方法用途: 签名加密<br>
- * 实现步骤: <br>
- * @param signStr :签名的字符串
- * @return
- */
- public static String encryptSigned(String signed) throws ComponentException {
-
- try {
- byte[] md5SignStr = MD5EncrtyReutrnhexDigitsByteArray(signed);
- String b64SignStr = BASE64Encrypt(md5SignStr);
- return b64SignStr;
- }catch(Exception e) {
- throw new ComponentException(e.getMessage()+ "BASE64或MD5加密签名错误!signed=" + signed + ", error=" + e.getMessage());
- }
- }
-
- public static void main(String[] args) throws Exception {
- System.out.println(encryptSigned("app"));
- System.out.println(encryptSigned("123"));
- }
-
- }
工程配置信息
application.yml配置:
- server:
- port: 9999
- spring:
- application:
- name: trade-auth
- # 配置中心
- cloud:
- # 注册中心配置
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
- config:
- server-addr: 127.0.0.1:8848
- # 数据源配置, 采用Druid
- datasource:
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- username: root
- password: 654321
- url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
- druid:
- # 连接池的配置信息
- # 初始化大小,最小,最大
- initial-size: 5
- min-idle: 5
- maxActive: 20
- # 配置获取连接等待超时的时间
- maxWait: 60000
- # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
- timeBetweenEvictionRunsMillis: 60000
- # 配置一个连接在池中最小生存的时间,单位是毫秒
- minEvictableIdleTimeMillis: 300000
- validationQuery: SELECT 1 FROM DUAL
- testWhileIdle: true
- testOnBorrow: false
- testOnReturn: false
- # 打开PSCache,并且指定每个连接上PSCache的大小
- poolPreparedStatements: true
- maxPoolPreparedStatementPerConnectionSize: 20
- # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
- filters: stat,wall,log4j
- # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
- connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
- # 配置DruidStatFilter
- web-stat-filter:
- enabled: true
- url-pattern: "/*"
- exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
- # 配置DruidStatViewServlet
- stat-view-servlet:
- url-pattern: "/druid/*"
- # IP白名单(没有配置或者为空,则允许所有访问)
- allow:
- # IP黑名单 (存在共同时,deny优先于allow)
- deny:
- # 禁用HTML页面上的“Reset All”功能
- reset-enable: false
- # 登录名
- login-username: admin
- # 登录密码
- login-password: admin123
- # 监控后台开关, 开启可通过后台管理查看
- enabled: true
- # Freemarker模板引擎配置
- freemarker:
- allow-request-override: false
- allow-session-override: false
- cache: true
- charset: UTF-8
- check-template-location: true
- content-type: text/html
- enabled: true
- expose-request-attributes: false
- expose-session-attributes: false
- expose-spring-macro-helpers: true
- prefer-file-system-access: true
- suffix: .ftl
- template-loader-path: classpath:/templates/
- # Spring Boot 的自动化配置, 排除过滤
- autoconfigure:
- exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
-
- # Jpa功能配置
- jpa:
- hibernate:
- ddl-auto: none
- naming:
- # 实际命名, 无转换
- physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
- show-sql: true
-
- # Redis 缓存配置
- redis:
- host: 127.0.0.1
- password:
- port: 6379
-
- ## spring security 配置
- security:
- oauth2:
- resource:
- loadBalanced: true
- token-info-uri: http://trade-auth/oauth/check_token
- client:
- client-id: app
- client-secret: app
- scope: server
- # 默认放行url,如果子模块重写这里的配置就会被覆盖
- ignore-urls:
- - /actuator/**
- - /v2/api-docs
- - /swagger-ui.html
- - /doc.html
这里包含Durid的数据源配置, 如果要开启后台监控页面, spring.datasource.druid.stat-view-servlet.enabled要设为true。
Freemarker模板引擎的配置, 这里我们使用了自定义的OAUTH login 页面。
Jpa功能的配置, 数据库和实体都采用驼峰命名方式, 不须再转换, physical-strategy选项值要设置为org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Redis的配置, 用于OAuth TokenStore 缓存以及Spring Cache缓存
Spring Security 认证配置, 我们采用支持负载方式配置, 增强可用性。
拷贝静态服务资源
这里采用了自定义的登陆页面, 将静态资源css与ftl模板copy至resources目录下:
数据库数据初始化:
-
- -- ----------------------------
- -- Table structure for t_oauth_client_details
- -- ----------------------------
- DROP TABLE IF EXISTS `t_oauth_client_details`;
- CREATE TABLE `t_oauth_client_details` (
- `client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
- `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,
- `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,
- `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,
- `refresh_token_validity` int(11) NULL DEFAULT NULL,
- `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,
- PRIMARY KEY (`client_id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-
- INSERT INTO `trade_stock`.`t_oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('admin', NULL, 'ISMvKXpXpadDiUoOSoAfww==', 'read_writer', 'password_refresh_token', NULL, NULL, NULL, NULL, NULL, NULL);
- INSERT INTO `trade_stock`.`t_oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('app', NULL, '0qV9wdiD/SH7mVFpnfccxw==', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true');
-
新增t_oauth_client_details表, 初始化两条client信息, 名称app, 密码app; 名称admin, 密码admin。
初始化t_trade_user表数据
- INSERT INTO `trade_stock`.`t_trade_user`(`id`, `userNo`, `name`, `userPwd`, `phone`, `companyId`, `email`, `address`, `lastLoginIp`, `lastLoginTime`, `status`, `craeteTime`) VALUES (1, 'admin', 'admin', 'ISMvKXpXpadDiUoOSoAfww==', '123', 1, NULL, NULL, NULL, NULL, 0, NULL);
-
在用户信息表t_trade_user里面初始化一条数据, 用户名为admin, 密码为admin。
启动相关服务
启动认证服务
输入完用户名与密码, admin/admin, 返回结果:
申请token
新建一个请求, 输入地址: 127.0.0.1:9999/oauth/token
注意, 最后不要加/结束, 不是127.0.0.1:9999/oauth/token/
选择认证方式, 填入Client的用户与密码信息,非t_trade_user信息。
填入grant_type, 认证模式; username和password对应t_trade_user表信息,scope作用域对应client用户的scope, 不能填错。
采用post和get方式请求都可以, 返回结果可以看到有两部分, 一部分是内置的token信息, 另外一部分就是我们自定义增强的token扩展信息。
刷新token, 地址不变, 需要传递refresh_token, 这个在申请token时可以拿到, grant_type类型要设置为refresh_token。
刷新成功, 和上面申请的token值发生了变化, access_token已重新生成。
改进用户服务, 实现用户登陆接口, 集成OAUTH2鉴权功能。
和最初搭建的用户服务已有较多差别, 加入了Mybatis,Druid数据源,Spring Security OAuth2集成。
为规范各微服务对异常的处理, 采用统一封装异常及错误码, 使用方法:
在stock-common-utils下面封装了两类异常:
一个是ComponentException为组件异常, 使用相对简单 ;
另一个是BusinessException为业务异常,相比组件异常,可以封装更多信息,便于日志分析排查。
错误码统一要实现IErrorCodeEnum接口, 不能随便定义, ApplicationErrorCodeEnum是一个错误码的实现类:
- public enum ApplicationErrorCodeEnum implements IErrorCodeEnum {
-
-
- SUCCESS("200", "成功"),
- FAILURE("300", "系统异常"),
- COMPONENT_LOAD_PROPERTIES_OBJ_HAD_EXIST("000001", "配置文件加载类已经存在" ),
- SYS_ERROR_ENCRYPT_SINGED(IErrorCodeEnum.MODULE_SYSTEM, "000002", "签名加密错误"),
-
- USER_NOT_FOUND(IErrorCodeEnum.MODULE_USER, "000003", "用户不存在!"),
- USER_PWD_ERROR(IErrorCodeEnum.MODULE_USER, "000004", "用户密码错误!"),
- ;
-
- /**
- * 业务模块
- */
- private String module;
-
- /**
- * 错误编号
- */
- private String code;
-
- /**
- * 消息
- */
- private String message;
-
- /**
- * 错误级别
- */
- private WarningLevelEnum warningLevel;
-
-
- ApplicationErrorCodeEnum(String code, String message, WarningLevelEnum warningLevelEnum) {
- this.code = code;
- this.message = message;
- this.warningLevel = warningLevelEnum;
- }
-
- ApplicationErrorCodeEnum(String module, String code, String message, WarningLevelEnum warningLevelEnum) {
- this.module = module;
- this.code = code;
- this.message = message;
- this.warningLevel = warningLevelEnum;
- }
-
- ApplicationErrorCodeEnum(String module, String code, String message) {
- this.module = module;
- this.code = code;
- this.message = message;
- this.warningLevel = WarningLevelEnum.COMMON;;
- }
-
-
- ApplicationErrorCodeEnum(String code, String message) {
- this.module = IErrorCodeEnum.MODULE_SYSTEM;
- this.code = code;
- this.message = message;
- this.warningLevel = WarningLevelEnum.COMMON;
- }
-
-
- @Override
- public String getCode() {
- return IErrorCodeEnum.MODULE_SYSTEM + this.code;
- }
-
- @Override
- public String getMessage() {
- return this.message;
- }
-
- @Override
- public WarningLevelEnum getLevel() {
- return warningLevel;
- }
-
-
- @Override
- public String toString() {
- return IErrorCodeEnum.MODULE_SYSTEM + this.code + ", " + this.message;
- }
- }
里面主要包含服务模块, 错误编号, 消息和错误级别信息, 便于我们规范性的根据错误码来排查系统的错误信息。
有了异常与错误码, 并非可以任意使用, 为避免调用一个接口, 同一个异常在多处抛出, 规范在service业务层统一处理异常, 由controller接入层捕获异常, 封装返回给调用方。
使用ApiRespResult统一返回数据,规范数据返回格式, 代码:
-
- /**
- * 统一API接口数据返回对象
- * @param <T>
- */
- public class ApiRespResult<T> implements Serializable {
-
- private static final long serialVersionUID = -1L;
-
- /**
- * 结果码
- */
- private String code = ApplicationErrorCodeEnum.SUCCESS.getCode();
-
- /**
- * 结果信息
- */
- private String msg = ApplicationErrorCodeEnum.SUCCESS.getMessage();
-
- /**
- * 扩展对象(放置分页信息、其他信息等)
- */
- private Object extendData;
-
- /**
- * 返回结果的数据对象
- */
- private T data;
-
- public ApiRespResult() {
- }
-
- public ApiRespResult(String code) {
- this.code = code;
- }
-
- public ApiRespResult(String code, String message){
- this.code = code;
- this.msg = message;
- }
-
-
- public ApiRespResult(IErrorCodeEnum errorCodeEnum){
- this.code = errorCodeEnum.getCode();
- this.msg = errorCodeEnum.getMessage();
- }
-
- public static <T> ApiRespResult<T> error(IErrorCodeEnum errorCodeEnum){
- return new ApiRespResult<T>(errorCodeEnum);
- }
-
- public static <T> ApiRespResult<T> sysError(String exceptionMsg){
-
- ApiRespResult error = new ApiRespResult<T>(ApplicationErrorCodeEnum.FAILURE);
- error.setMsg(error.getMsg() + ":" + exceptionMsg);
- return error;
- }
-
- public static <T> ApiRespResult<T> error(String code, String msg){
- return new ApiRespResult<T>(code,msg);
- }
-
- public static <T> ApiRespResult<T> error(String code, String msg,T data){
- return new ApiRespResult<T>(code,msg).setData(data);
- }
-
- public static ApiRespResult<Void> success(){
- return success(null);
- }
-
- public static <T> ApiRespResult<T> success(T data){
- return success(data, null);
- }
-
- public static <T> ApiRespResult<T> success(T data, Object extendData ){
- return new ApiRespResult<T>().setData(data).setExtendData(extendData);
- }
-
- public Boolean isSuccess(){
- return ApplicationErrorCodeEnum.SUCCESS.getCode().equals(getCode());
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public String getMsg() {
- return msg;
- }
-
- public ApiRespResult<T> setMsg(String msg) {
- this.msg = msg;
- return this;
- }
-
-
- public Object getExtendData() {
- return extendData;
- }
-
- public ApiRespResult<T> setExtendData(Object extendData) {
- this.extendData = extendData;
- return this;
- }
-
- public T getData() {
- return data;
- }
-
- public ApiRespResult<T> setData(T data) {
- this.data = data;
- return this;
- }
-
- @Override
- public String toString() {
- return "[code=" + code + ", msg=" + msg + ", extendData=" + extendData + ", data=" + data + "]";
- }
- }
-
主要封装了结果码, 结果信息, 数据对象和扩展信息, 返回数据示例:
- {
- "code": "SYS_200",
- "msg": "成功",
- "extendData": null,
- "data": {
- "id": null,
- "userNo": "admin",
- "name": "admin",
- "userPwd": "ISMvKXpXpadDiUoOSoAfww==",
- "phone": "123",
- "companyId": 1,
- "email": null,
- "address": null,
- "lastLoginIp": null,
- "lastLoginTime": null,
- "status": 0,
- "craeteTime": null
- },
- "success": true
- }
用户服务, 添加pom依赖
- <dependencies>
- <!-- spring boot 依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!-- Nacos服务注册发现依赖 -->
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
- </dependency>
- <!-- Spring Boot 监控组件依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-actuator</artifactId>
- </dependency>
- <!-- 公用数据层组件 -->
- <dependency>
- <groupId>com.itcast.trade</groupId>
- <artifactId>bulls-stock-common-dao</artifactId>
- <version>${project.stock.version}</version>
- </dependency>
-
- <!-- 公用实体层依赖 -->
- <dependency>
- <groupId>com.itcast.trade</groupId>
- <artifactId>bulls-stock-entity</artifactId>
- <version>${project.stock.version}</version>
- </dependency>
- <!-- 公用WEB层依赖 -->
- <dependency>
- <groupId>com.itcast.trade</groupId>
- <artifactId>bulls-stock-common-web</artifactId>
- <version>${project.stock.version}</version>
- </dependency>
-
- <!-- 公用的工具依赖 -->
- <dependency>
- <groupId>com.itcast.trade</groupId>
- <artifactId>bulls-stock-common-utils</artifactId>
- <version>${project.stock.version}</version>
- </dependency>
-
- <!-- feign 远程调用 -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-openfeign</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-openfeign-core</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
-
- <!-- OAUTH2 LoadBalance 功能相关依赖 -->
- <dependency>
- <groupId>com.netflix.archaius</groupId>
- <artifactId>archaius-core</artifactId>
- <version>0.7.6</version>
- </dependency>
-
- <!-- Spring Security OATUH2 鉴权相关依赖 -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-oauth2</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-security</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.security.oauth.boot</groupId>
- <artifactId>spring-security-oauth2-autoconfigure</artifactId>
- <version>${spring-cloud-starter.version}</version>
- </dependency>
- </dependencies>
common-dao工程下面的POM依赖:
- <!-- Spring Boot Mybatis依赖 -->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.1.0</version>
- </dependency>
-
- <!-- druid 数据库连接池 -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>${druid.version}</version>
- </dependency>
-
- <!-- 解决druid log4j监控组件无法使用问题, 不用再额外增加其他LOG框架 -->
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </dependency>
-
- <!-- mysql-connector-java -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <version>8.0.14</version>
- </dependency>
-
启动类StockUserApplication
- @SpringBootApplication
- @EnableDiscoveryClient
- @EnableFeignClients
- @ComponentScan(basePackages = {"com.itcast"})
- @MapperScan("com.itcast.trade.bulls.stock.user.dao")
- @EnableTransactionManagement
- public class StockUserApplication {
-
- public static void main(String[] args) {
-
- SpringApplication.run(StockUserApplication.class, args);
- }
-
- }
MapperScan是Mybatis注解,配置扫描路径
EnableTransactionManagement注解是开启事务支持
数据层
用户服务数据接口IStockUserDao
- @Repository
- public interface IStockUserDao {
-
-
- /**
- * 根据用户账号获取用户对象
- * @param userNo
- * @return
- */
- TradeUser getByUserNo(String userNo);
-
- }
Mapper定义StockUserMapper.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.itcast.trade.bulls.stock.user.dao.IStockUserDao">
- <resultMap id="BaseResultMap" type="com.itcast.bulls.stock.entity.user.TradeUser">
- <result column="id" jdbcType="BIGINT" property="id" />
- <result column="userNo" jdbcType="VARCHAR" property="userNo" />
- <result column="name" jdbcType="VARCHAR" property="name" />
- <result column="userPwd" jdbcType="VARCHAR" property="userPwd" />
- <result column="phone" jdbcType="VARCHAR" property="phone" />
- <result column="companyId" jdbcType="BIGINT" property="companyId" />
- <result column="email" jdbcType="VARCHAR" property="email" />
- <result column="address" jdbcType="VARCHAR" property="address" />
- <result column="lastLoginIp" jdbcType="VARCHAR" property="lastLoginIp" />
- <result column="lastLoginTime" jdbcType="TIMESTAMP" property="lastLoginTime" />
- <result column="status" jdbcType="TINYINT" property="status" />
- <result column="craeteTime" jdbcType="TIMESTAMP" property="craeteTime" />
- </resultMap>
-
- <select id="getByUserNo" resultMap="BaseResultMap">
- select
- userNo, name, userPwd,
- phone, companyId, email,
- address, lastLoginIp, lastLoginTime,
- status, craeteTime
- from t_trade_user
- where userNo = #{userNo}
- </select>
-
-
- </mapper>
服务层StockUserServiceImpl
- @Service
- @Log4j2
- public class StockUserServiceImpl implements IStockUserService {
-
-
- @Autowired
- private IStockUserDao stockUserDao;
-
- /**
- * 用户登陆
- * @param userNo
- * @param userPwd
- * @return
- */
- public TradeUser userLogin(String userNo, String userPwd) throws ComponentException {
-
- // 获取用户对象
- TradeUser tradeUser= stockUserDao.getByUserNo(userNo);
- if(null == tradeUser) {
- throw new ComponentException(ApplicationErrorCodeEnum.USER_NOT_FOUND);
- }
-
- // 用户密码加密判断
- String encryptPassword = EncryptUtil.encryptSigned(userPwd);
- boolean pwdMatch= tradeUser.getUserPwd().equals(encryptPassword);
- if(!pwdMatch) {
- log.error(ApplicationErrorCodeEnum.USER_PWD_ERROR);
- throw new ComponentException(ApplicationErrorCodeEnum.USER_PWD_ERROR);
- }
-
- return tradeUser;
- }
- }
用户登陆接口实现逻辑。
接入层StockUserController
- @RestController()
- @RequestMapping("/user")
- @Log4j2
- public class StockUserController {
-
- @Autowired
- private IStockUserService stockUserService;
-
- /**
- * 用户登陆接口
- * @param userNo
- * @param userPwd
- * @return
- */
- @RequestMapping("/userLogin")
- public ApiRespResult userLogin(@RequestParam("userNo")String userNo, @RequestParam("userPwd") String userPwd) {
-
- ApiRespResult result = null;
- try {
- // 用户登陆逻辑处理
- TradeUser tradeUser = stockUserService.userLogin(userNo, userPwd);
- result = ApiRespResult.success(tradeUser);
- }catch(ComponentException e) {
- log.error(e.getMessage(), e);
- result = ApiRespResult.error(e.geterrorCodeEnum());
- }catch(Exception e) {
- log.error(e.getMessage(), e);
- result = ApiRespResult.sysError(e.getMessage());
- }
-
- return result;
-
- }
-
- }
-
配置类
认证配置ResourceSecurityConfigurer
-
- @Primary
- @Order(90)
- @Configuration
- @EnableResourceServer
- @EnableGlobalMethodSecurity(prePostEnabled = true)
- public class ResourceSecurityConfigurer implements ResourceServerConfigurer {
-
- @Autowired
- protected RemoteTokenServices remoteTokenServices;
-
- @Autowired
- private RestTemplate lbRestTemplate;
-
- /**
- * 远程调用,采用restTemplate方式处理
- * @param resourceServerSecurityConfigurer
- * @throws Exception
- */
- @Override
- public void configure(ResourceServerSecurityConfigurer resourceServerSecurityConfigurer) throws Exception {
- remoteTokenServices.setRestTemplate(lbRestTemplate);
- resourceServerSecurityConfigurer.tokenServices(remoteTokenServices);
-
- }
-
- /**
- * 资源服务安全配置
- * @param httpSecurity
- * @throws Exception
- */
- @Override
- public void configure(HttpSecurity httpSecurity) throws Exception {
-
- httpSecurity.csrf().disable()
- .authorizeRequests()
- .antMatchers("/user/**").authenticated().and()
- .formLogin().loginPage("/login")
- .failureUrl("/login?error")
- .defaultSuccessUrl("/home");
- }
-
- /**
- * RestTemplate配置
- * @return
- */
- @Bean
- @Primary
- @LoadBalanced
- public RestTemplate lbRestTemplate() {
- RestTemplate restTemplate = new RestTemplate();
- restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
- @Override
- public void handleError(ClientHttpResponse response) throws IOException {
- if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {
- super.handleError(response);
- }
- }
- });
- return restTemplate;
- }
-
- }
用户服务为资源服务, 认证采用RestTemplate调用方式。 资源服务一定要开启@EnableResourceServer注解, @EnableGlobalMethodSecurity为方法级别安全控制。
工程配置
配置文件bootstrap.yml
- server:
- port: 10681
- spring:
- application:
- name: stock-user
- cloud:
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
- config:
- server-addr: 127.0.0.1:8848
-
- # 数据源配置, 采用Druid
- datasource:
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- username: root
- password: 654321
- url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
- druid:
- # 连接池的配置信息
- # 初始化大小,最小,最大
- initial-size: 5
- min-idle: 5
- maxActive: 20
- # 配置获取连接等待超时的时间
- maxWait: 60000
- # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
- timeBetweenEvictionRunsMillis: 60000
- # 配置一个连接在池中最小生存的时间,单位是毫秒
- minEvictableIdleTimeMillis: 300000
- validationQuery: SELECT 1 FROM DUAL
- testWhileIdle: true
- testOnBorrow: false
- testOnReturn: false
- # 打开PSCache,并且指定每个连接上PSCache的大小
- poolPreparedStatements: true
- maxPoolPreparedStatementPerConnectionSize: 20
- # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
- filters: stat,wall,log4j
- # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
- connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
- # 配置DruidStatFilter
- web-stat-filter:
- enabled: true
- url-pattern: "/*"
- exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
- # 配置DruidStatViewServlet
- stat-view-servlet:
- url-pattern: "/druid/*"
- # IP白名单(没有配置或者为空,则允许所有访问)
- allow:
- # IP黑名单 (存在共同时,deny优先于allow)
- deny:
- # 禁用HTML页面上的“Reset All”功能
- reset-enable: false
- # 登录名
- login-username: admin
- # 登录密码
- login-password: admin123
- # 监控后台开关, 开启可通过后台管理查看
- enabled: true
-
- ## spring security 配置
- security:
- oauth2:
- resource:
- loadBalanced: true
- token-info-uri: http://trade-auth/oauth/check_token
- client:
- client-id: app
- client-secret: app
- scope: server
- access-token-uri: http://trade-auth/oauth/token
- user-authorization-uri: http://trade-auth/oauth/authorize
-
- #mybatis 配置
- mybatis:
- mapper-locations: classpath:com/itcast/trade/stock/user/dao/mapper/*.xml
-
启动Nacos、Redis和trade-auth认证服务
启动用户服务, 端口为10681, 访问登陆接口: 127.0.0.1:10681/user/userLogin?userNo=admin&userPwd=admin
在没有认证,不传递token的情况下, 返回错误信息:
上面演示了如何使用Druid数据源, 大家应该有了一个大概了解, 这里再扩展一下, 了解下它的功能, 以及监控台的使用。
Druid是一个非常优秀的数据库连接池。在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。并且Druid在阿里巴巴部署了超过600个应用, 是经过了严苛的生产环境检验, 具有较强的可靠性。
Durid监控台包含数据源配置信息、SQL监控、防火墙信息、URI监控、Session监控和Spring 监控等。
配置 | 缺省值 | 说明 |
---|---|---|
name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:”DataSource-” + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。 | |
url | 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | |
username | 连接数据库的用户名 | |
password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。 | |
driverClassName | 根据url自动识别 | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName |
initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
maxActive | 8 | 最大连接池数量 |
maxIdle | 8 | 已经不再使用,配置了也没效果 |
minIdle | 最小连接池数量 | |
maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
maxPoolPreparedStatementPerConnectionSize | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句,常用select ‘x’。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 | |
validationQueryTimeout | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 | |
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 1分钟(1.0.14) | 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
numTestsPerEvictionRun | 30分钟(1.0.14) | 不再使用,一个DruidDataSource只支持一个EvictionRun |
minEvictableIdleTimeMillis | 连接保持空闲而不被驱逐的最长时间 | |
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall | |
proxyFilters | 类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |
显示详细信息
上面搭建了认证服务和用户资源服务, 能够成功使用OAUTH2进行认证,作为微服务, 需要由网关进行统一转发处理, 接下来我们改进网关服务, 通过路由转发支持OAUTH2的使用。
经过实践摸索,如果网关只是转发,按照我们OAUTH2的设计方案, Spring Cloud Gateway 可以不用集成Spring Security。网关的职责就是接收客户端的请求并进行转发, 所以鉴权可以不用放置在网关, 各微服务直接作为资源服务进行认证,也可以避免微服务直接对外暴露产生的安全问题, 在这里学习如何通过Gateway转发请求, 实现OAuth2的认证。
备注: 工程当中存有摸索实践的代码与配置, 已经注释,可以忽略, 如后续扩展, 可以参考。
用户服务工程集成Druid监控台配置, bootstrap.yml
- spring:
- # 数据源配置, 采用Druid
- datasource:
- type: com.alibaba.druid.pool.DruidDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- username: root
- password: 654321
- url: jdbc:mysql://192.168.19.150:3306/trade_stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false
- druid:
- # 连接池的配置信息
- # 初始化大小,最小,最大
- initial-size: 5
- min-idle: 5
- maxActive: 20
- # 配置获取连接等待超时的时间
- maxWait: 60000
- # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
- timeBetweenEvictionRunsMillis: 60000
- # 配置一个连接在池中最小生存的时间,单位是毫秒
- minEvictableIdleTimeMillis: 300000
- validationQuery: SELECT 1 FROM DUAL
- testWhileIdle: true
- testOnBorrow: false
- testOnReturn: false
- # 打开PSCache,并且指定每个连接上PSCache的大小
- poolPreparedStatements: true
- maxPoolPreparedStatementPerConnectionSize: 20
- # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
- filters: stat,wall,log4j
- # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
- connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
- # 配置DruidStatFilter
- web-stat-filter:
- enabled: true
- url-pattern: "/*"
- exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
- # 配置DruidStatViewServlet
- stat-view-servlet:
- url-pattern: "/druid/*"
- # IP白名单(没有配置或者为空,则允许所有访问)
- allow:
- # IP黑名单 (存在共同时,deny优先于allow)
- deny:
- # 禁用HTML页面上的“Reset All”功能
- reset-enable: false
- # 登录名
- login-username: admin
- # 登录密码
- login-password: admin123
- # 监控后台开关, 开启可通过后台管理查看
- enabled: true
主要是stat-view-servlet下面配置:
url-pattern是配置监控台的访问地址。
allow是允许哪些IP进行访问。
deny是IP黑名单, 优先级高于allow。
reset-enable是复位功能, 谨慎使用。
login-username是登陆了用户名。
login-password是登陆密码。
enabled是监控台的启用开关, 此项一定要开启。
启动类StockGatewayApplication
- @SpringBootApplication
- @EnableDiscoveryClient
- @ComponentScan(basePackages = {"com.itcast"})
- public class StockGatewayApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(StockGatewayApplication.class, args);
- }
- }
全局过滤器StockRequestGlobalFilter
- @Component
- @Log4j2
- public class StockRequestGlobalFilter implements GlobalFilter, Ordered {
-
-
- /**
- * 通过filter来自定义配置转发信息
- * @param exchange
- * @param chain
- * @return
- */
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- String authentication = exchange.getRequest().getHeaders().getFirst("Authorization");
- if(!StringUtil.isNullOrEmpty(authentication)){
- log.info("enter stockRequestGlobalFilter filter method: " + authentication);
- exchange.getRequest().mutate().header("Authorization",authentication);
- }
- return chain.filter(exchange.mutate().build());
- }
-
- @Override
- public int getOrder() {
- return -1000;
- }
- }
这是自定义全局过滤器的实现, 防止header中的Authorization没有转发的问题。
工程配置application.yml
- server:
- port: 10680
-
- spring:
- application:
- name: stock-gateway
- cloud:
- nacos:
- discovery:
- server-addr: 127.0.0.1:8848
- service: stock-gateway
- gateway:
- discovery:
- # 允许通过服务名称进行路由转发访问, http://service-id/user
- locator:
- enabled: true
- # 路由配置
- routes:
- - id: stock-user
- uri: lb://stock-user
- predicates:
- - Path=/user/**
- - id: trade-auth
- uri: lb://trade-auth
- predicates:
- # - Method=GET,POST 不要开启此项
- - Path=/oauth/**
- logging:
- level:
- root: info
-
-
我们定义了两个路由, 一个是stock-user,映射路径为/user开头, 转发至用户服务; 另一个是 trade-auth, 映射路径为/oauth, 转发至认证服务。
注意不要开启Method=GET,POST, 默认允许所有请求方式, 认证会用到。
启动Gateway网关服务, 端口为10680。
通过网关申请token, 访问地址: 127.0.0.1:10680/oauth/token
与请求认证服务一样, 输入token申请参数,可以看到, 通过网关访问, 能够成功返回token数据。
增加Bearer Token, 通过网关请求用户服务, 也能够成功返回登陆数据。
通过以上验证, 网关能够正常路由转发, 对外我们只需提供一个地址, 客户端即可访问认证服务与受保护的资源服务。
启动服务即可访问, 把用户服务启动, 访问地址: http://127.0.0.1:10681/druid/index.html
成功启动, 可以看到监控台信息, 这个在生产环境中, 如果遇到问题时, 可以开启, 帮助我们定位分析线上的故障。
学会 Spring Cloud Gateway的集成与配置, 通过Gateway代理OAUTH2认证服务, 申请TOKEN, 并请求资源服务,对外统一地址服务, 通过路径映射。
了解Durid连接池的作用, 掌握集成配置, 通过Druid控制后台, 实时监控查看数据库连接状况, 根据提供的信息, 能够尽快定位和排查线上的问题。
实践过程当中会碰到各种问题, 这里例举一些, 避免大家碰到类似情况。
为什么认证报出401错误?
首先检查数据库脚本是否完整, t_oauth_client_details表的初始化数据是否正确;
其次检查用户名和加密密码是否匹配, 可以通过debug跟踪查看, 是不是字符编码等问题引起。
为什么用户名和密码输入正确, 仍不能申请token?
需要在认证服务的配置中开启allowFormAuthenticationForClients,允许进行form请求认证;
可以在redis中清除client_detail缓存,一般是oauth开头的键值, 不能找到就执行flushall全部清除。
为什么资源服务通过token调用, 会出现超时情况?
检查认证服务是否在nacos成功注册;
检查资源服务的security配置中, loadbalance是否开启;
检查资源服务的安全配置,是否定义了RestTemplate, 且RemoteTokenServices需要注入RestTemplate。
Gateway网关调用认证服务, GET请求正常, POST请求失败, 出现404错误?
在网关工程的配置中, 检查predicates下的Method配置, 要允许所有请求类型, 可以将此项配置去除, 默认是允许所有请求类型。
其他资料参考:
oauth2 授权流程:
http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/en/Security/OAuth.html
10分钟理解OAUTH2协议
https://deepzz.com/post/what-is-oauth2-protocol.html
Spring Boot OAuth2.0密码模式服务器实现
https://blog.csdn.net/qq_34873338/article/details/80218212
Redis plus 客户端工具:
https://pan.baidu.com/s/1ETwWnEj4rbsE1S3GlYHlWg#list/path=%2F
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。