在阅读本文章前,我假设读者已经有了OAuth2、SpringBoot和Spring Security基础。下面我将会从编码的顺序来讲,如何通过Spring Security OAuth2逐步实现OAuth2密码模式。
一、Maven 依赖
这里只列出一些主要的jar包。
1.SpringBoot
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/>
</parent>
2.Spring Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.Spring Security OAuth2
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.8.RELEASE</version>
</dependency>
4.Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
5.lombok
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
6.Swagger
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
二、Spring Security OAuth2 表结构
以下俩张表必须要有,这里我用的是Oracle。另外,可以根据业务添加其他的拓展字段。
1.应用表
create table oauth_client_details (
client_id varchar2(128) not null
constraint pk_oauth_client_details
primary key,
resource_ids varchar2(128) default null,
client_secret varchar2(128) default null,
scope varchar2(128) default null,
authorized_grant_types varchar2(128) default null,
web_server_redirect_uri varchar2(128) default null,
authorities varchar2(128) default null,
access_token_validity number(9) default null,
refresh_token_validity number(9) default null,
additional_information varchar2(4000) default null,
autoapprove varchar2(128) default null
);
comment on table oauth_client_details is '应用表';
comment on column oauth_client_details.client_id is '应用id';
comment on column oauth_client_details.resource_ids is '授权资源id集合';
comment on column oauth_client_details.client_secret is '应用密钥';
comment on column oauth_client_details.scope is '授权作用域';
comment on column oauth_client_details.authorized_grant_types is '授权允许类型';
comment on column oauth_client_details.web_server_redirect_uri is '授权回调地址';
comment on column oauth_client_details.authorities is '拥有权限集合';
comment on column oauth_client_details.access_token_validity is 'access_token有效期(秒)';
comment on column oauth_client_details.refresh_token_validity is 'refresh_token有效期(秒)';
comment on column oauth_client_details.additional_information is '附加信息(预留)';
comment on column oauth_client_details.autoapprove is '自动同意授权(true,false,read,write)';
2.用户表
create table client_user (
user_id number(9) not null
constraint pk_client_user
primary key,
username varchar2(128),
password varchar2(128),
client_id varchar2(128),
status number(1)
);
comment on table client_user is '用户表';
comment on column client_user.user_id is '用户id';
comment on column client_user.username is '用户名';
comment on column client_user.password is '密码';
comment on column client_user.client_id is '应用id';
comment on column client_user.status is '状态:0-已禁用,1-已启用';
alter table client_user add constraint uq_client_user unique (username);
create sequence seq_client_user
maxvalue 999999999;
/
三、Java代码
1.配置Swagger
SwaggerConfig.java
package com.leven.auth.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* swagger文档配置
* @author Leven
* @date 2020-04-15
*/
@Configuration
@EnableSwagger2
@Profile("dev")
public class SwaggerConfig {
/**
* 添加摘要信息(Docket)
*/
@Bean
public Docket controllerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("OAuth2之密码模式")
.description("接口文档")
.contact(new Contact("Leven", null, "leven0106@163.com"))
.version("V1.0")
.build())
.select()
.apis(RequestHandlerSelectors.basePackage("com.leven.auth.api.controller"))
.paths(PathSelectors.any())
.build();
}
@Bean
public UiConfiguration uiConfiguration() {
return UiConfigurationBuilder.builder().defaultModelsExpandDepth(0).build();
}
}
2.配置Redis
RedisConfig.java
package com.leven.auth.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis缓存配置
* @author Leven
* @date 2020-04-08
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
3.配置Security
WebSecurityConfiguration.java
package com.leven.auth.api.security;
import com.leven.auth.api.security.custom.CustomAuthenticationProvider;
import com.leven.auth.model.constant.AuthConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import java.util.HashMap;
import java.util.Map;
/**
* spring security核心配置
* @author Leven
* @date 2019-08-06
*/
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>(2);
idToPasswordEncoder.put(AuthConstant.ID_FOR_ENCODE_DEFAULT, new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder(AuthConstant.ID_FOR_ENCODE_DEFAULT, idToPasswordEncoder);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(customAuthenticationProvider);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 不需要拦截的请求
.antMatchers("/**").permitAll()
// CSRF防护
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
CustomAuthenticationProvider.java
package com.leven.auth.api.security.custom;
import com.leven.commons.model.exception.SPIException;
import com.leven.auth.core.service.ClientUserService;
import com.leven.auth.model.constant.AuthExceptionCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义认证服务实现
* @author Leven
* @date 2019-08-06
*/
@Slf4j
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private ClientUserService clientUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String username = token.getName();
UserDetails userDetails = null;
if (username != null) {
userDetails = clientUserService.loadUserByUsername(username);
}
if (userDetails == null) {
throw new SPIException(AuthExceptionCode.USERNAME_NOT_FOUND);
} else if (!userDetails.isEnabled()) {
throw new SPIException(AuthExceptionCode.ACCOUNT_DISABLED);
} else if (!userDetails.isAccountNonExpired()) {
throw new SPIException(AuthExceptionCode.ACCOUNT_EXPIRED);
} else if (!userDetails.isAccountNonLocked()) {
throw new SPIException(AuthExceptionCode.ACCOUNT_LOCKED);
} else if (!userDetails.isCredentialsNonExpired()) {
throw new SPIException(AuthExceptionCode.CREDENTIALS_EXPIRED);
}
//数据库存储的密文
String password = userDetails.getPassword();
//用户输入的明文
String plainPwd = (String)token.getCredentials();
if (StringUtils.isBlank(plainPwd)) {
throw new SPIException(AuthExceptionCode.PWD_IS_EMPTY);
}
//校验密码是否正确
if (!passwordEncoder.matches(plainPwd, password)) {
throw new SPIException(AuthExceptionCode.BAD_CREDENTIALS);
}
//授权
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> aClass) {
//返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
}
ClientUserService
package com.leven.auth.core.service;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* 授权用户接口
* @author Leven
* @date 2019-03-20
*/
public interface ClientUserService extends UserDetailsService {
}
ClientUserServiceImpl
package com.leven.auth.core.service.impl;
import com.leven.commons.model.exception.SPIException;
import com.leven.auth.core.mapper.ClientUserMapper;
import com.leven.auth.core.service.ClientUserService;
import com.leven.auth.model.constant.AuthExceptionCode;
import com.leven.auth.model.enums.ClientUserStatusEnum;
import com.leven.auth.model.pojo.dto.ClientUserDTO;
import com.leven.auth.model.pojo.query.ClientUserQuery;
import com.leven.auth.model.pojo.security.ClientUserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 授权用户接口实现
* @author Leven
* @date 2019-03-20
*/
@Service
public class ClientUserServiceImpl implements ClientUserService, UserDetailsService {
@Autowired
private ClientUserMapper clientUserMapper;
/**
* 根据用户名查找授权用户
* @param username 用户名
* @return 授权应用用户详情
* @throws UsernameNotFoundException 用户不存在异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ClientUserQuery clientUser = new ClientUserQuery();
clientUser.setUsername(username);
ClientUserDTO userDTO = clientUserMapper.getByQuery(clientUser);
if (userDTO == null) {
throw new SPIException(AuthExceptionCode.CLIENT_USER_NOT_EXISTS);
}
//设置用户
boolean enabled = ClientUserStatusEnum.BAN.getValue() != userDTO.getStatus();
return new ClientUserDetails(userDTO, enabled, true, true,
true, null);
}
}
4.配置授权服务
AuthorizationServerConfiguration.java
package com.leven.auth.api.security;
import com.leven.auth.api.security.custom.CustomAccessDeniedHandler;
import com.leven.auth.api.security.custom.CustomTokenEnhancer;
import com.leven.auth.api.security.custom.CustomWebResponseExceptionTranslator;
import com.leven.auth.core.service.ClientUserService;
import com.leven.auth.model.constant.AuthConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
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.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.sql.DataSource;
/**
* Oauth2.0 认证授权服务配置
* @author Leven
* @date 2019-08-02
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private ClientUserService clientUserService;
@Autowired
private CustomWebResponseExceptionTranslator webResponseExceptionTranslator;
/**
* 注入authenticationManager
* 来支持 password grant type
*/
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomTokenEnhancer customTokenEnhancer;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
/**
* 声明TokenStore实现
* @return
*/
@Bean
public TokenStore tokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(AuthConstant.APPLICATION + ":");
return tokenStore;
}
/**
* 声明 ClientDetails实现
* @return
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler);
}
@SuppressWarnings("unchecked")
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager);
endpoints.userDetailsService(clientUserService);
endpoints.exceptionTranslator(webResponseExceptionTranslator);
endpoints.tokenStore(tokenStore());
endpoints.tokenEnhancer(customTokenEnhancer);
}
}
CustomWebResponseExceptionTranslator.java
package com.leven.auth.api.security.custom;
import com.leven.commons.core.web.bean.OuterResult;
import com.leven.commons.model.exception.BasicEcode;
import com.leven.commons.model.exception.SPIException;
import com.leven.auth.model.constant.AuthExceptionCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.stereotype.Component;
/**
* 认证相关的异常处理
* @author Leven
* @date 2019-08-07
*/
@Slf4j
@Component
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity translate(Exception e) {
log.error("web response exception translator:", e);
OuterResult result;
if (e instanceof SPIException) {
SPIException spi = (SPIException) e;
result = new OuterResult(spi.getEcode(), spi.getMsg());
} else {
result = new OuterResult(AuthExceptionCode.OAUTH2_ERROR, BasicEcode.getMsg(AuthExceptionCode.OAUTH2_ERROR));
}
return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
}
}
CustomTokenEnhancer.java
package com.leven.auth.api.security.custom;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义Token增强
* @author Leven
* @date 2019-08-07
*/
@Component
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
if (accessToken instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken token = ((DefaultOAuth2AccessToken) accessToken);
Map<String, Object> additionalInformation = new HashMap<>(1);
additionalInformation.put("expiration", token.getExpiration());
token.setAdditionalInformation(additionalInformation);
return token;
}
return accessToken;
}
}
CustomAccessDeniedHandler.java
package com.leven.auth.api.security.custom;
import com.leven.auth.api.helper.ResponseHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 权限相关的异常处理
* @author Leven
* @date 2019-08-02
*/
@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
log.error("access denied exception:", e);
ResponseHelper.fail(request, response, HttpStatus.FORBIDDEN.getReasonPhrase(), e.getMessage());
}
}
ResponseHelper.java
package com.leven.auth.security.custom;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.leven.commons.core.web.bean.OuterResult;
import com.leven.commons.model.exception.BasicEcode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义响应JSON
* @author Leven
*/
@Slf4j
public class ResponseHelper {
private static final String CONTENT_TYPE = "application/json;charset=UTF-8";
private ResponseHelper(){}
/**
* 成功返回结果
* @param response
* @param obj
* @throws IOException
*/
public static void success(HttpServletResponse response, Object obj) {
print(response, BasicEcode.SUCCESS, null, obj);
}
/**
* 异常返回结果
* @param response
* @param ecode
* @throws IOException
*/
public static void error(HttpServletResponse response, String ecode) {
error(response, ecode, BasicEcode.getMsg(ecode));
}
/**
* 异常返回结果
* @param response
* @param ecode
* @throws IOException
*/
public static void error(HttpServletResponse response, String ecode, String msg) {
print(response, ecode, msg, null);
}
/**
* 异常返回结果
* @param response
* @param ecode
* @throws IOException
*/
public static void error(HttpServletResponse response, String ecode, Object... args) {
print(response, ecode, null, null, args);
}
private static void print(HttpServletResponse response, String ecode, String msg, Object data, Object... args) {
OuterResult result = OuterResult.newInstance();
result.setEcode(ecode);
if (StringUtils.isBlank(msg)) {
msg = BasicEcode.getMsg(ecode);
}
if (args != null && args.length > 0) {
msg = String.format(msg, args);
}
result.setMsg(msg);
result.setData(data);
response.setContentType(CONTENT_TYPE);
try {
response.getWriter().print(JSON.toJSONString(result, SerializerFeature.WriteMapNullValue));
} catch (IOException e) {
log.error("打印返回结果报错:", e);
}
}
}
5.配置资源服务
ResourceServerConfiguration.java
package com.leven.auth.api.security;
import com.leven.auth.api.security.custom.CustomAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* Oauth2.0 资源服务配置
* @author Leven
* @date 2019-08-02
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler);
}
}
6.Swagger文档过滤拦截:排除静态资源过滤
WebMvcConfiguration.java
package com.leven.auth.api.security;
import com.leven.auth.api.interceptor.SecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* spring mvc配置
* @author Leven
* @date 2019-08-05
*/
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired
private SecurityInterceptor transformInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(transformInterceptor)
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**")
.addPathPatterns("/**");
}
}
7.启动类
AuthApiApplication.java
package com.leven.auth.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tk.mybatis.spring.annotation.MapperScan;
/**
* SpringBoot启动类
* @author Leven
* @date 2019-03-20
*/
@SpringBootApplication(scanBasePackages = {"com.leven"})
@MapperScan(basePackages = { "com.leven.auth.core.mapper" })
@ServletComponentScan
@EnableTransactionManagement
@EnableAsync
public class AuthApiApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApiApplication.class, args);
}
}
8.数据层(tk.mybatis)和application.yml我就不写了......
四、后续
本文为思否原创文章,未经允许不得转载。
如读者发现有错误,欢迎留言!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。