# oauthserver **Repository Path**: qiaokang666/oauthserver ## Basic Information - **Project Name**: oauthserver - **Description**: 快速实现Spring Boot Oauth2授权服务,保护你的应用资源。- 源码分析 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 99 - **Created**: 2020-05-09 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # oauthserver - [配置启动](https://gitee.com/qiaokang666/oauthserver#%E9%85%8D%E7%BD%AE%E5%90%AF%E5%8A%A8) - [源码阅读](https://gitee.com/qiaokang666/oauthserver#%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB) - [【SmsIntegrationAuthenticator】的类图](https://gitee.com/qiaokang666/oauthserver#smsintegrationauthenticator%E7%9A%84%E7%B1%BB%E5%9B%BE) - [【IntegrationAuthenticator】类图](https://gitee.com/qiaokang666/oauthserver#integrationauthenticator%E7%B1%BB%E5%9B%BE) - [【Handler】处理器类图](https://gitee.com/qiaokang666/oauthserver#handler%E5%A4%84%E7%90%86%E5%99%A8%E7%B1%BB%E5%9B%BE) - [【Spring Security配置】类图](https://gitee.com/qiaokang666/oauthserver#spring-security%E9%85%8D%E7%BD%AE%E7%B1%BB%E5%9B%BE) - [【使用@Slf4j优化日志注入】](https://gitee.com/qiaokang666/oauthserver#%E6%B3%A8%E8%A7%A3slf4j%E7%9A%84%E4%BD%BF%E7%94%A8) - [【使用@Order管理配置加载顺序】](https://gitee.com/qiaokang666/oauthserver#order%E7%9A%84%E4%BD%BF%E7%94%A8) - [【UrlAuthorityAspect】类图](https://gitee.com/qiaokang666/oauthserver#urlauthorityaspect%E7%B1%BB%E5%9B%BE) - AOP完成后台权限验证 - [【@InitBinder注解使用】](https://gitee.com/qiaokang666/oauthserver#initbinder%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8---%E5%BC%95%E5%87%BA%E4%B8%A4%E7%A7%8D%E6%9C%BA%E5%88%B6)-- 引出两种机制 - [【@EqualsAndHashCode注解】](https://gitee.com/qiaokang666/oauthserver#equalsandhashcode%E6%B3%A8%E8%A7%A3) - [【@FeignClient】](https://gitee.com/qiaokang666/oauthserver#feignclient) - [【JpaRepository】](https://gitee.com/qiaokang666/oauthserver#jparepository) - [【支持多种类型数据库,数据源配置】](https://gitee.com/qiaokang666/oauthserver#%E6%94%AF%E6%8C%81%E5%A4%9A%E7%A7%8D%E7%B1%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93%E6%95%B0%E6%8D%AE%E6%BA%90%E9%85%8D%E7%BD%AE) - 使用jpa和mybatis完成支持多种类型数据库,不是多数据源 - [【@Primary 和 @Qualifier】](https://gitee.com/qiaokang666/oauthserver#primary-%E5%92%8C-qualifier) - [【认证和授权】](https://gitee.com/qiaokang666/oauthserver#%E8%AE%A4%E8%AF%81%E5%92%8C%E6%8E%88%E6%9D%83) - [【oauthserver授权机制】](https://gitee.com/qiaokang666/oauthserver#oauthserver%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6) - [【@EnableDiscoveryClient与@EnableEurekaClient区别】](https://gitee.com/qiaokang666/oauthserver#enablediscoveryclient%E4%B8%8Eenableeurekaclient%E5%8C%BA%E5%88%AB) - [【@EnableAsync@Async】](https://gitee.com/qiaokang666/oauthserver#enableasyncasync)- 异步调用 ## 配置启动 1. 配置数据库 1. 安装mysql数据库 2. 建立数据库thymelte,编码为utf8 3. 顺序导入【SQL初始化】schema-mysql.sql,schema-mysql-20190613.sql 4. 修改项目数据库配置 模块common中找到application-mysql.yml,修改密码password为自己的代码 2. 配置redis缓存数据库 1. 安装redis数据库 2. 配置redis数据库密码123456 3. 配置项目redis缓存配置 模块common中找到application-common.yml, a、46行-55行的注释去掉 b、59行type修改为redis c、找到EhcacheConfig类注释注解 ```java //@Configuration //@EnableCaching ``` d、找到RedisConfig类打开注解 ```java @Configuration @EnableCaching ``` e、49行密码修改为123456 3. 启动eureka-server模块 运行EurekaServerApplication类 4. 启动web模块 访问地址:http://localhost:8182 用户名:18800000000 密码:1234567890c 5. ## 源码阅读 ### 【SmsIntegrationAuthenticator】的类图 ![image-20200509150144024](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200509150144024.png)**实现【ApplicationEventPublisherAware】接口的作用【发布事件,可以监听事件进行自动注册用户】** 举例简单说明: **需求:** ​ 系统要求,当用户注册后,给他发送一封邮件通知他注册成功了。然后给他初始化积分,发放一张新用户注册优惠券等 **设计:** 1. **定义一个用户注册事件。** ```java public class UserRegisterEvent extends ApplicationEvent{ public UserRegisterEvent(String name) { //name即source super(name); } } ``` ApplicationEvent 是由 Spring 提供的所有 Event 类的基类,为了简单起见,注册事件只传递了 name(可以复杂的对象,但注意要了解清楚序列化机制)。 2. **定义一个用户注册服务(事件发布者)。** ```java @Service public class UserService implements ApplicationEventPublisherAware { public void register(String name) { System.out.println("用户:" + name + " 已注册!"); applicationEventPublisher.publishEvent(new UserRegisterEvent(name)); } private ApplicationEventPublisher applicationEventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } } ``` 需要注意的是,服务必须交给 Spring 容器托管。ApplicationEventPublisherAware 是由 Spring 提供的用于为 Service 注入 ApplicationEventPublisher 事件发布器的接口,使用这个接口,我们自己的 Service 就拥有了发布事件的能力。用户注册后,不再是显示调用其他的业务 Service,而是发布一个用户注册事件。 3. **创建邮件服务,积分服务,其他服务(事件订阅者)等。** ```java @Service public class EmailService implements ApplicationListener { @Override public void onApplicationEvent(UserRegisterEvent userRegisterEvent) { System.out.println("邮件服务接到通知,给 " + userRegisterEvent.getSource() + " 发送邮件..."); } } ``` 事件订阅者的服务同样需要托管于 Spring 容器,ApplicationListener 接口是由 Spring 提供的事件订阅者必须实现的接口,我们一般把该 Service 关心的事件类型作为泛型传入。处理事件,通过 event.getSource() 即可拿到事件的具体内容,在本例中便是用户的姓名。 其他两个 Service,也同样编写,实际的业务操作仅仅是打印一句内容即可,篇幅限制,这里省略。 4. 最后我们使用 Springboot 编写一个启动类。 ```java @SpringBootApplication @RestController public class EventDemoApp { public static void main(String[] args) { SpringApplication.run(EventDemoApp.class, args); } @Autowired UserService userService; @RequestMapping("/register") public String register(){ userService.register("xttblog.com"); return "success"; } } ``` ### 【IntegrationAuthenticator】类图 ![image-20200509160007503](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200509160007503.png) 1. 定义认证接口【IntegrationAuthenticator】规定了认证流程 2. 定义抽象类【AbstractPreparableIntegrationAuthenticator】处理一些共用业务 3. 定义了两种方式【用户名密码认证】和【手机号验证码认证】 ### 【Handler】处理器类图 ![image-20200509162853296](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200509162853296.png) 1. 【AuthSuccessHandler】认证成功后到此处理器,记录日志,跳转到首页 2. 【CustomAuthenticationFailureHandler】认证失败后,跳转到登录页,并提示错误 ### 【Spring Security配置】类图 ![image-20200509163407616](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200509163407616.png) 1. 授权框架Spring Security的配置 ```java @Slf4j @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private AuthSuccessHandler authSuccessHandler; @Autowired private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return authenticationManager(); } @Override protected void configure(HttpSecurity http) throws Exception { //logoutSuccessUrl如果设为/login,那么退出后, //会重新自动创建session,触发OnLineCountListener的sessionCreated方法,造成在线人数不准。 http .headers().frameOptions().sameOrigin() .httpStrictTransportSecurity().disable() .and() .formLogin() .successHandler(authSuccessHandler) .loginPage("/login").permitAll() .failureHandler(customAuthenticationFailureHandler) } } ``` **注入了上面的Handler** ### 【注解@Slf4j的使用】 ​ 声明:如果不想每次都写private final Logger logger = LoggerFactory.getLogger(当前类名.class); 可以用注解@Slf4j; ​ 注意上面需要引入lombok插件才可以使用 ### 【@Order的使用】 ​ Spring 4.2.3 利用`@Order`控制配置类的加载顺序 ​ 如果配置之间有依赖关系,可以通过@ConditionalOnBean来指定依赖关系 ### 【UrlAuthorityAspect】类图 AOP的使用,用来判断Url是否有权限,来完成后台控制 ![image-20200511094542293](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511094542293.png) 拦截所有controller的方法判断权限 ```java @Around(value = "getMappingAspect() || postMappingAspect() || deleteMappingAspect() || patchMappingAspect() || requestMappingAspect()") public Object around(ProceedingJoinPoint pjp) throws Throwable { //通过uuid关联请求参数和返回参数 String uuid = UUID.randomUUID().toString().replaceAll("-", ""); //String uuid = ""; methodBefore(pjp, uuid); try { Object proceed = pjp.proceed(); methodAfterReturning(proceed, uuid); return proceed; } catch (Exception e) { log.error("[{}]Response异常内容:{}", uuid, e); throw e; } } private void methodBefore(JoinPoint joinPoint, String uuid) { // 接收到请求,记录请求内容 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录下请求内容 //log.info("[{}]请求地址:{}", uuid, request.getRequestURI()); //log.info("[{}]请求方法:{}", uuid, request.getMethod()); //权限判断 String requiredAuthority = sideMenuService.findAuthorityByUrlAndRequestMethod(request.getRequestURI(), request.getMethod()); if (StringUtils.isNotEmpty(requiredAuthority)){ Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); UserEntity userEntity = null; if(principal instanceof UserEntity){ userEntity = (UserEntity)principal; } List userAuthorities = userEntity.getAuthorities(); boolean flag = false; for(int i = 0; i < userAuthorities.size(); i++){ if (requiredAuthority.contains(userAuthorities.get(i).getAuthority())){ flag = true; break; } } if (!flag){ throw new AccessDeniedException("您没有权限访问"); } } // 打印请求内容 try { // 下面两个数组中,参数值和参数名的个数和位置是一一对应的。 Object[] objs = joinPoint.getArgs(); // 参数名 String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); Map paramMap = new LinkedHashMap<>(); for (int i = 0; i < objs.length; i++) { if (!(objs[i] instanceof ExtendedServletRequestDataBinder) && !(objs[i] instanceof HttpServletResponseWrapper)) { paramMap.put(argNames[i], objs[i]); } } if (paramMap.size() > 0) { //log.info("[{}]方法:{}", uuid, joinPoint.getSignature()); //log.info("[{}]参数:{}", uuid, JSONObject.toJSONString(paramMap)); } } catch (Exception e) { //log.error("[{}]AOP methodBefore:", uuid, e); } } ``` ### 【@InitBinder注解使用】-- 引出两种机制 ​ 从字面意思可以看出这个的作用是给Binder做初始化的,被此注解的方法可以对WebDataBinder初始化。webDataBinder是用于表单到方法的数据绑定的! ​ 尝用来对日期数据格式做绑定,如下所示 ```java @InitBinder protected void initBinder(WebDataBinder binder) { binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"))); } }); binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); } }); binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern("HH:mm:ss"))); } }); } ``` **spring类型自动转换——@InitBinder和Converter** spring有2种类型转换器,一种是propertyEditor,一种是Converter.虽然都是类型转换,但是还是有细微差别. PropertyEditor是String到类型,这只是一种特殊情况的转换,而说到最通用的肯定是类型到类型.此时就是Converter的应用了. **比如:** ​ 我前台上传一张图片实际上以MultipartFile类型传到Controller中的,spring如何将MultipartFile转换成byte[]自动封装成PoDemo类呢?此时PropertyEditor显然不行了.这已经是类型到类型转换,只能用Converter. ​ 可能有人想到Converter实际上是包含PropertyEditor的,那么如果2种转换器都适用,那么究竟会适用哪种呢?Spring默认是首先查找PropertyEditor,然后再查找Converter. 总结:PropertyEditor适用于String到类型,而Converter更加通用用于类型到类型.PropertyEditor优先级更高. ### 【@EqualsAndHashCode注解】 原文中提到的大致有以下几点: 1. 此注解会生成equals(Object other) 和 hashCode()方法。 2. 它默认使用非静态,非瞬态的属性 3. 可通过参数exclude排除一些属性 4. 可通过参数of指定仅使用哪些属性 5. 它默认仅使用该类中定义的属性且不调用父类的方法 6. 可通过callSuper=true解决上一点问题。让其生成的方法中调用父类的方法。 另:@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。 通过官方文档,可以得知,当使用@Data注解时,则有了@EqualsAndHashCode注解,那么就会在此类中存在equals(Object other) 和 hashCode()方法,且不会使用父类的属性,这就导致了可能的问题。 比如,有多个类有相同的部分属性,把它们定义到父类中,恰好id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,却因为lombok自动生成的equals(Object other) 和 hashCode()方法判定为相等,从而导致出错。 修复此问题的方法很简单: 1. 使用@Getter @Setter @ToString代替@Data并且自定义equals(Object other) 和 hashCode()方法,比如有些类只需要判断主键id是否相等即足矣。 2. 或者使用在使用@Data时同时加上@EqualsAndHashCode(callSuper=true)注解。 ### 【@FeignClient】 JAVA 项目中接口调用怎么做 ? - Httpclient - Okhttp - Httpurlconnection - RestTemplate 我们今天要介绍的用法比上面的更简单,方便,它就是[Feign](https://github.com/OpenFeign/feign) Feign是一个声明式的REST客户端,它的目的就是让REST调用更加简单。 SpringCloud对Feign进行了封装,使其支持SpringMVC标准注解和HttpMessageConverters。 Feign可以与Eureka和Ribbon组合使用以支持负载均衡。 **区别:** 在预订微服务中,有一个同步呼叫票价(Fare)。RestTemplate是用来制作的同步呼叫。使用RestTemplate时,URL参数是以编程方式构造的,数据被发送到其他服务。在更复杂的情况下,我们将不得不RestTemplate深入到更低级别的API提供的甚至是API 的细节。 Feign是Spring Cloud Netflix库,用于在基于REST的服务调用上提供更高级别的抽象。Spring Cloud Feign在声明性原则上工作。使用Feign时,我们在客户端编写声明式REST服务接口,并使用这些接口来编写客户端程序。开发人员不用担心这个接口的实现。这将在运行时由Spring动态配置。通过这种声明性的方法,开发人员不需要深入了解由HTTP提供的HTTP级别API的细节的RestTemplate。 **如何使用?** 定义一个接口 ```java package com.simon.client; import com.simon.common.domain.ResultMsg; import com.simon.model.QuartzJob; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; /** * @author simon * @version 1.0 * @date 2019-09-23 10:22 */ @FeignClient(name = "old-task",fallback = TaskClientFallBack.class) public interface TaskClient { /** * 获取所有任务 * @return 所有任务 */ @GetMapping("/api/tasks") ResultMsg> getAllJobs(); } ``` 定义一个实现类 ```java package com.simon.client; import com.simon.common.domain.ResultCode; import com.simon.common.domain.ResultMsg; import com.simon.model.QuartzJob; import org.springframework.stereotype.Component; import java.util.List; /** * @author simon * @version 1.0 * @date 2019-09-23 10:32 */ @Component public class TaskClientFallBack implements TaskClient { @Override public ResultMsg> getAllJobs() { return ResultMsg.fail(ResultCode.FAIL); } } ``` ### 【JpaRepository】 ### 【支持多种类型数据库,数据源配置】 数据源 ```java package com.simon.common.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.apache.ibatis.mapping.DatabaseIdProvider; import org.apache.ibatis.mapping.VendorDatabaseIdProvider; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.util.Properties; /** * 数据源配置 * @author simon * @date 2019-02-18 */ @Configuration public class DataSourceConfig { @Value("${mybatis.mapper-locations}") private String mapperLocations; @Primary @Bean @ConfigurationProperties("spring.datasource.druid") public DataSource dataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean public JdbcTemplate jdbcTemplate(){ return new JdbcTemplate(dataSource()); } @Bean public DatabaseIdProvider databaseIdProvider(){ DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); Properties p = new Properties(); p.setProperty("Oracle", "oracle"); p.setProperty("MySQL", "mysql"); p.setProperty("PostgreSQL", "postgresql"); p.setProperty("DB2", "db2"); p.setProperty("SQL Server", "sqlserver"); databaseIdProvider.setProperties(p); return databaseIdProvider; } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); factoryBean.setDatabaseIdProvider(databaseIdProvider()); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); return factoryBean; } } ``` 配置文件 application-common.yml ```yaml spring: profiles: active: mysql #pg, mysql, oracle,sqlserver。默认使用MySQL数据库连接配置。 datasource: type: com.alibaba.druid.pool.DruidDataSource druid: max-active: 20 initial-size: 1 max-wait: 60000 min-idle: 3 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 remove-abandoned: true remove-abandoned-timeout: 180 connection-properties: clientEncoding=UTF-8 test-while-idle: false test-on-borrow: true test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,log4j use-global-data-source-stat: true web-stat-filter: enabled: true stat-view-servlet: enabled: true # login-username: simon # login-password: 19961120 reset-enable: true allow: 127.0.0.1 url-pattern: /druid/* jpa: hibernate: naming: #解决如果Column注解定义的字段名和属性名一样,会被忽略的问题。 physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl ddl-auto: none #validate show-sql: true properties: hibernate: temp: use_jdbc_metadata_defaults: false ``` mysql配置文件 application-mysql.yml ```yaml spring: datasource: tomcat: init-s-q-l: SET NAMES utf8mb4 # MySQL连接信息 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/thymelte?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC #解决异常【 The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized】 username: ENC(YiYjVwTulDGN//YaB3KbuA==) #明文root,可以直接用username: root password: ENC(YiYjVwTulDGN//YaB3KbuA==) #明文19941017,可以直接用password: 19941017 druid: validation-query: SELECT 1 connection-init-sqls: SET NAMES utf8mb4 jpa: database-platform: org.hibernate.dialect.MySQL57Dialect #pagehelper分页插件 pagehelper: #禁用reasonable分页参数合理化,页码小于1或者大于最大页码返回空数据 reasonable: false params: count=countSql # oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012 helper-dialect: mysql support-methods-arguments: true ``` 配置多种数据库的 application-xx.yml,然后通过配置profiles的active为xx来决定使用哪种数据库 ```java package com.simon.common.service.impl; @Slf4j public abstract class CrudServiceImpl, ID extends java.io.Serializable> implements CrudService { @Autowired private SqlSessionFactory sqlSessionFactory; @Autowired protected CrudMapper crudMapper; @Autowired protected JpaRepository jpaRepository; protected Class poType; @SuppressWarnings("unchecked") public CrudServiceImpl() { ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); poType = (Class) pt.getActualTypeArguments()[0]; } @Override public long count() { return jpaRepository.count(); } @Override public T save(T model) { return jpaRepository.save(model); } @Override public List save(List list) { return jpaRepository.save(list); } @Override public List findAll() { return jpaRepository.findAll(); } @Override public Page findAll(Pageable pageable) { return jpaRepository.findAll(pageable); } @Override public void delete(ID id) { jpaRepository.delete(id); } @Override public int deleteByIds(String ids) { return crudMapper.deleteByIds(ids); } @Override public T findById(ID id) { return jpaRepository.findOne(id); } @Override public int insert(T model) { return crudMapper.insert(model); } @Override public int insertSelective(T model) { return crudMapper.insertSelective(model); } @Override public int insertList(List list) { return crudMapper.insertList(list); } @Override public int updateByPrimaryKey(T model) { return crudMapper.updateByPrimaryKey(model); } @Override public int updateByPrimaryKeySelective(T model) { return crudMapper.updateByPrimaryKeySelective(model); } @Override public void batchSave(List list) { } @Override public void batchUpdate(List list) { } @Override public PageInfo findAll(Integer pageNo, Integer pageSize, String orderBy) { if (null == pageSize) { pageSize = AppConfig.DEFAULT_PAGE_SIZE; } orderBy = orderBy.trim(); if (StringUtils.isEmpty(orderBy)) { PageHelper.startPage(pageNo, pageSize); } else { PageHelper.startPage(pageNo, pageSize, orderBy); } List list = crudMapper.selectAll(); return new PageInfo<>(list); } @Override public PageInfo getList(Map params, Integer pageNo, Integer pageSize, String orderBy) { if (null == pageSize) { pageSize = AppConfig.DEFAULT_PAGE_SIZE; } orderBy = orderBy.trim(); if (StringUtils.isEmpty(orderBy)) { PageHelper.startPage(pageNo, pageSize); } else { PageHelper.startPage(pageNo, pageSize, orderBy); } List list = crudMapper.getList(params); return new PageInfo<>(list); } } ``` 基础类是jpa和mybatis混用 ### 【@Primary 和 @Qualifier】 **问题** 当一个接口有2个不同实现时,使用@Autowired注解时会报 ​ org.springframework.beans.factory.NoUniqueBeanDefinitionException异常信息 **解决** (1)使用Qualifier注解,选择一个对象的名称,通常比较常用 (2)Primary可以理解为默认优先选择,不可以同时设置多个,内部实质是设置BeanDefinition的primary属性 注解 备注 @Primary 优先方案,被注解的实现,优先被注入 @Qualifier 先声明后使用,相当于多个实现起多个不同的名字,注入时候告诉我你要注入哪个使用实例 一:`@Qualifier` 接口: ```java public interface EmployeeService { public EmployeeDto getEmployeeById(Long id); } ``` 接口对应的两个实现类:EmployeeServiceImpl和EmployeeServiceImpl1:接口对应的两个实现类:EmployeeServiceImpl和EmployeeServiceImpl1: ```java @Service("employeeService") public class EmployeeServiceImpl implements EmployeeService { public EmployeeDto getEmployeeById(Long id) { return new EmployeeDto(); } } ``` ```java @Service("employeeService1") public class EmployeeServiceImpl1 implements EmployeeService { public EmployeeDto getEmployeeById(Long id) { return new EmployeeDto(); } } ``` **这个时候就要用到@Qualifier注解了,qualifier的意思是合格者,通过这个标示,表明了哪个实现类才是我们所需要的,我们修改调用代码,添加@Qualifier注解,需要注意的是@Qualifier的参数名称必须为我们之前定义@Service注解的名称之一!** ```java @Controller @RequestMapping("/emplayee.do") public class EmployeeInfoControl { @Autowired @Qualifier("employeeService") EmployeeService employeeService; @RequestMapping(params = "method=showEmplayeeInfo") public void showEmplayeeInfo(HttpServletRequest request, HttpServletResponse response, EmployeeDto dto) { //#略 } } ``` @Primary:和@Qualifier 一样,@Primary也一样,使用场景经常是:在spring 中使用注解,常使用@Autowired, 默认是根据类型Type来自动注入的。但有些特殊情况,对同一个接口,可能会有几种不同的实现类,而默认只会采取其中一种的情况下 @Primary 的作用就出来了。 **接口:** ```java public interface Singer { String sing(String lyrics); } ``` ```java @Component // 加注解,让spring识别 public class MetalSinger implements Singer{ @Override public String sing(String lyrics) { return "I am singing with DIO voice: "+lyrics; } } ``` ```java @Primary @Component public class OperaSinger implements Singer{ @Override public String sing(String lyrics) { return "I am singing in Bocelli voice: "+lyrics; } } ``` ### 【认证和授权】 ​ 我们在搭建微服务的时候,常常需要考虑的一个问题是,微服务之间以及你的应用和微服务之间是怎么信任对方的。 ​ 这个时候我们会谈到两个概念,认证(authentication)和授权(authorization)。这是两个不同的概念,通俗点讲,认证是指系统需要确认**你是谁?**,而授权是指在通过认证之后,**你能干什么?**,多数场景下这里的“你”指的都是第三方应用程序。 #### **认证** 说到认证,我们可以先来了解下Http常用的一些认证方式。 **Http Basic认证** 其中**最简单的是Http Basic认证方式**,这种认证方式是讲用户名密码按照格式**“用户名:密码”**通过Base-64编码成一个hash值,然后通过Authorization header传递到服务端,然后服务端再通过同样的Base-64编码方式进行解码成为**“用户名:密码”**格式进行认证。 ```shell Authorization: Basic QWxhZGRpbjfdaGVuIHNlc2FtZQ== ``` 这种认证方式的缺点就是需要在每个Http请求中将用户名密码在网络中进行传输,很容易被破解。而且如果每个Http请求都需要传输用户凭证的话,被破解的概率越高,所以这种认证方式通常需要结合Https进行使用。 **Http Digest认证** 而更为安全一点的认证方式是Http Digest认证方式,它也是通过Authorization header来进行传递的,但是算法会更加复杂和不可逆。 ```shell Authorization: Digest username="abc", realm="testing@bb.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41" ``` Http Digest的认证过程为,当客户端第一次请求服务端资源时,服务端会返回一个随机数(nonce), 然后客户端会通过多次MD5加密来计算出来response的值 (response=MD5(HA1:nonce:HA2)), 其中HA1=MD5(username:realm:password), HA2=MD5(method:digestURI). 当服务端拿到这个response,那么它会从DB取出用户名密码来做同样的操作来看计算出来的response是否一致,如果一致,则表明认证通过。 这种认证方式也会有一定的局限性,那就是**服务端不能不可逆的加密用户的密码。** **Cookies & Session** 由于上面认证方式的缺陷,所以现在常见的认证方式是只需要在第一次登陆请求中传递用户名密码,服务端在校验结束后生成一个session-id,并将这个session-id和用户关联,然后通过http response的cookie header返回给客户端,客户端只需要存储这个cookie并在后续的请求都带上这个cookie就可以。这样服务端也可以对用户的密码进行不可逆加密后存储。 这样的认证方式也会存在一些问题,比如如果web客户端不允许存储cookie呢?如果服务器down了,那么在cache中存储的session也会丢失。所以一般会采用Token的方式来进行认证,比如JWT。 #### **授权** 关于授权,现在最流行的就是OAuth2了,它是一个授权框架而不是一个认证框架,它的一个目的就是为了减少在各个请求中用户名密码的传递。关于什么是OAuth2,这里有一篇最简向导可以很快帮助你理解什么是OAuth2 - [The Simplest Guide To OAuth 2.0](https://medium.com/@darutk/the-simplest-guide-to-oauth-2-0-8c71bd9a15bb)。而关于OAuth2的几种workflow,上面那篇最简向导的作者也写了一篇,以图例的方式很详细的介绍了OAuth2实现的几种workflow - [Diagrams And Movies Of All The OAuth 2.0 Flows](https://medium.com/@darutk/diagrams-and-movies-of-all-the-oauth-2-0-flows-194f3c3ade85) 当我们知道了什么是OAuth2以及它的几种workflow后,我们就需要知道什么样的情况下需要采用哪种workflow。在极客时间的杨波老师给了一个流程图来帮助判断什么样的场景下需要采用哪种OAuth2的workflow。 ![image-20200511140842373](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511140842373.png) **授权服务器(Authorization Server)** ![image-20200511140920817](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511140920817.png) 在OAuth2的四大角色中,我们最不熟悉的就是授权服务器了。因为app以及resource server就是我们平常开发的前端和后端,app会访问resource server的REST API去获取数据来展示。而授权服务器会是什么?它需要我们自己实现吗? 其实在微服务架构中,一般都需要有自己的一个授权服务器,它的作用主要是分发token给不同的调用方,然后它们可以使用这个token去访问相应的微服务。 我们可以把授权服务器看成是提供一组REST API的service: 1. 授权API(/oauth/authorize) - 这个API会对调用方请求进行授权,返回一个authorization code。 2. 获取Token API(/oauth/token) - 这个API会根据客户请求传入的authorization code来生成一个access token并返回。 3. 校验Token API(/oauth/introspect) - 这个API一般会是resource server用来校验请求方的access token是否有效。 4. 撤销Token API(/oauth/revoke) - 这个API 会把access token直接撤销。 ![image-20200511145751266](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511145751266.png) 大多数情况下我们都需要实现自己的authentication server,好在spring 框架提供了一个基于spring security 的oauth框架来帮助实现对应的authentication server, resource server 以及client。 ![image-20200511141016104](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511141016104.png) 从上图可以看到,对于spring security提供的默认authorization server的默认实现,其实就是提供了两个endpoint - TokenEndpoint (提供/oauth/token api)和 AuthorizationEndpoint(提供/oauth/authorize api). #### Authorization Server和Resource Server 实战 下面我们可以用spring boot来实现一个authorization server。注意:spring boot版本选用1.5.2.RELEASE 版本。 首先生成一个Springboot 工程,下面是运行主类和build.gradle 配置。 ```java @SpringBootApplication public class AuthserverApplication { public static void main(String[] args) { SpringApplication.run(AuthserverApplication.class, args); } } ``` build.gradle ```json buildscript { ext { springBootVersion = '1.5.2.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'com.interview.authorization' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-security') compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.security.oauth:spring-security-oauth2') testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('org.springframework.security:spring-security-test') } ``` 上面的依赖文件主要是添加了spring security 和 spring security oauth2的依赖包。 **接下来是enable authorization server的配置类,** ```java @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("clientapp") .secret("123456").authorizedGrantTypes("authorization_code") .redirectUris("http://localhost:9000/callback").scopes("read_users"); } } ``` 这里主要使用了@EnableAuthorizationServer注解来告诉spring框架自动配置一些关于AuthorizationEndpoint以及一些关于AuthorizationServer security的配置。同时,来配置访问的client的一些细节 1. client - 实际上就是client id,要访问resource server的client,比如说mobile app or web page等等。 2. secret - 对应client的secret 3. grant type - 指的是实现oauth2的那几种workflow,authorization_code, implicit, password, client_credential 4. scopes - 授权的范围,可以自己定义一些字符串来表示。 最后需要在*application.properties*里面配置上访问/oauth/authorize 接口时需要的用户凭证 - 实际上就是resource owner的用户名,密码。 ```properties security.user.name=kevin security.user.password=123 ``` 接下来我们可以启动spring boot程序,**然后我们可以通过浏览器模拟client进行/oauth/authorize API的访问**, ```shell http://localhost:9090/oauth/authorize?client_id=clientapp&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_users ``` 这时浏览器会提示输入用户的用户名和密码,就是我们配置在application.properties文件里面的username和password ![image-20200511142339248](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511142339248.png) 输入用户名密码后,Authorization Server会跳转到一个授权界面: ![image-20200511141853621](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511141853621.png) 当你确认授权后会自动跳转到callback url 并附带一个authorization code, ```shell http://localhost:9000/callback?code=1TRZMN ``` 拿到authorization code之后,我们可以通过postman来模拟发送POST请求到/oauth/token接口 ```shell http://localhost:9090/oauth/token?code=1TRZMN&grant_type=authorization_code&redirect_uri=http://localhost:9000/callback&scope=read_users ``` 同时要加上Authorization Header,采用的是basic的验证方式,用户名密码则是配置在AuthorizationServerConfiguration类里面的client和secret ![image-20200511142008040](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511142008040.png) 最后可以拿到用于访问resource server的access token: ```json { "access_token": "823c7d7e-8294-4288-b4c3-46358ccff0fd", "token_type": "bearer", "expires_in": 32884, "scope": "read_users" } ``` 接下来我们就可以创建我们的resource server了,它所依赖的lib跟authorization server一样, ```json dependencies { compile('org.springframework.boot:spring-boot-starter-security') compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.security.oauth:spring-security-oauth2') testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('org.springframework.security:spring-security-test') } ``` 同时,resource server本身会暴露一些REST API给client 调用,这个就是我们需要保护的资源,也就是说,在访问/api/users API时,需要通过oauth2认证才可以调用该API,如下: ```java @RestController public class UserRestAPI { @RequestMapping("/api/users") public ResponseEntity> getUsers(){ List users = new ArrayList(); users.add(new User("kevin",33)); users.add(new User("joe",30)); return ResponseEntity.ok(users); } } public class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } ``` **接下来我们会配置关于resource server的配置,** ```java @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and().requestMatchers().antMatchers("/api/**"); } @Primary @Bean public RemoteTokenServices tokenService(){ RemoteTokenServices remoteTokenServices = new RemoteTokenServices(); remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9090/oauth/check_token"); remoteTokenServices.setClientId("clientapp"); remoteTokenServices.setClientSecret("123456"); return remoteTokenServices; } } ``` 首先是使用@EnableResourceServer注解,它会告诉spring框架自动配置一些关于resource server的配置,比如启用OAuth2AuthenticationProcessingFilter来检查进来的request有没有有效的accesstoken。 其次,我们需要配置那些request,那些API需要被认证后才能被访问。 最后,我们自己定义了一个RemoteTokenServices, 这个是用来跟Authorization Server进行打交道的,主要是用来校验发到resource server的accesstoken是否有效。在这个RemoteTokenServices中,我们需要传递clientId和clientSecret作为basic认证(Authorization Server需要), 它也指定了Authorization Server在哪里,需要访问的CheckTokenURL是什么。 如果这个时候你去访问resource server的API - /api/users 你会得到一个403 forbidden。因为你还没有在Authorization Server里面配置/oauth/check_token的访问权限,默认是"denyAll". ```java @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("clientapp") .secret("123456").authorizedGrantTypes("authorization_code") .redirectUris("http://localhost:9000/callback").scopes("read_users"); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("isAuthenticated()"); } } ``` 上面配置了/oauth/check_token的访问权限为必须是认证过的用户才可以访问 - `security.checkTokenAccess("isAuthenticated()");` 这个时候再去调用http://localhost:9091/api/users 就会返回正确的数据。 ![image-20200511142246481](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511142246481.png) #### JWT token 在上面的实战中,可以看到,每次resource server在拿到access token后都需要连接到authorization server去检查access token是否有效。为了支持在resource server中进行access token的自校验,我们可以使用JWT token。同时,JWT token还可以包含更多的元数据,可以是自己定义的,比如userId等这些不敏感的信息。想要了解JWT token的一些细节,可以参考 [JSON Web Token 入门教程](http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html)。 ### 【oauthserver授权机制】 oauth模块是授权服务器 - @EnableAuthorizationServer ```java package com.simon; @SpringBootApplication @MapperScan("com.simon.mapper") @EnableAsync @EnableAuthorizationServer @EnableDiscoveryClient @EnableTransactionManagement @EnableAspectJAutoProxy(proxyTargetClass = true) public class OauthApplication implements CommandLineRunner { public static void main(String[] args) { TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); ApplicationContext applicationContext = SpringApplication.run(OauthApplication.class, args); SpringBeanLoader.setApplicationContext(applicationContext); // new SpringApplicationBuilder(Application.class).web(true).run(args); } @Override public void run(String... strings) throws Exception { } } ``` api模块是资源服务器 - @EnableResourceServer ```java package com.simon; @SpringBootApplication @MapperScan("com.simon.mapper") @EnableAsync @EnableResourceServer @EnableOAuth2Client @EnableDiscoveryClient @EnableTransactionManagement @EnableAspectJAutoProxy(proxyTargetClass = true) @EnableEurekaClient public class ApiApplication implements CommandLineRunner { public static void main(String[] args) { TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); ApplicationContext applicationContext = SpringApplication.run(ApiApplication.class, args); SpringBeanLoader.setApplicationContext(applicationContext); // new SpringApplicationBuilder(Application.class).web(true).run(args); } @Override public void run(String... strings) throws Exception { } @Bean @LoadBalanced public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); //解决RestTemplate使用PATCH方法报错 HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setConnectTimeout(5000); requestFactory.setReadTimeout(5000); restTemplate.setRequestFactory(requestFactory); return restTemplate; } } ``` ### 【@EnableDiscoveryClient与@EnableEurekaClient区别】 在使用**Spring Cloud feign**使用中在使用服务发现的时候提到了两种注解,一种为**@EnableDiscoveryClient**,一种为**@EnableEurekaClient**,用法上基本一致。 ```html spring cloud中discovery service有许多种实现(eureka、consul、zookeeper等等),@EnableDiscoveryClient基于spring-cloud-commons, @EnableEurekaClient基于spring-cloud-netflix。 其实用更简单的话来说,就是如果选用的注册中心是eureka,那么就推荐@EnableEurekaClient,如果是其他的注册中心,那么推荐使用@EnableDiscoveryClient。 ``` ### 【@EnableAsync@Async】 使用多线程,往往是创建Thread,或者是实现runnable接口,用到线程池的时候还需要创建Executors,spring中有十分优秀的支持,就是注解@EnableAsync就可以使用多线程,@Async加在线程任务的方法上(需要异步执行的任务),定义一个线程任务,通过spring提供的ThreadPoolTaskExecutor就可以使用线程池 定义配置类 ```java @Configurable @EnableAsync public class TreadPoolConfigTest implements AsyncConfigurer{ @Override public Executor getAsyncExecutor() { // TODO Auto-generated method stub ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。 executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()); //最大线程数量 executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()*5); //线程池的队列容量 executor.setQueueCapacity(Runtime.getRuntime().availableProcessors()*2); //线程名称的前缀 executor.setThreadNamePrefix("this-excutor-"); // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务 // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行 //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } /*异步任务中异常处理*/ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { // TODO Auto-generated method stub return new SimpleAsyncUncaughtExceptionHandler(); } } ``` 线程任务类 ```java @Component public class TreadTasks { @Async public void startMyTreadTask() { System.out.println("this is my async task"); } } ``` 调用异步线程任务 ```java @Controller public class AsyncTaskUse { @Autowired private TreadTasks treadTasks; @GetMapping("/startMysync") public void useMySyncTask() { treadTasks.startMyTreadTask(); } } ``` ### 【Spring Cloud Web框架微服务翘楚】 ![image-20200511172931209](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511172931209.png) Eureka: **功能:服务注册与发现,各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里** 他有三端构成 1.Eureka Server 服务注册与发现中心端 2.Service Provider服务提供者端 3.Service Consumer服务调用者端 ![image-20200511173222169](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511173222169.png) Ribbon: **功能:服务请求调用客户端负载均衡,服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台** 客户端负载均衡,一个服务部署多台机器的情况feign不知道调用那台服务,那么ribbon就可以使用默认abab的轮询算法,给确定那一台机器 Feign: **功能:*服务请求调用,*基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求** 接口调用,restful风格http可以用调java接口方式去调用。 自己组装url与参数还有请求头等信息,不用自己去写啦。简单易用。 看下调用方式与代码 ```java //url=http://www.me.com @FeignClient(url = "${feign.order-promotion.url}") public interface PromotionClient { @RequestMapping(value = "/Member/Promotion/ReleasePromtionByOrderId", method = RequestMethod.POST) BaseWcfResponse ReleasePromtionByOrderId(@RequestBody ReleasePromtionReq request); } ``` ![image-20200511173343394](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511173343394.png) Hystrix: **功能:熔断器,发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题** 熔断器,个别服务接口挂拉,可能会影响整个服务链路,导致整体服务不可用,这个时候hystrix就派上用场啦。 ![image-20200511173455939](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511173455939.png) Zuul: **功能:api路由网关,如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务** 前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务 ![image-20200511173524612](https://gitee.com/qiaokang666/oauthserver/raw/master/tutorial/screenshots/image-20200511173524612.png) Sleuth: **功能:服务链路追踪,主要功能就是在分布式系统中提供追踪解决方案,并且兼容支持了 zipkin**