目录
- 环境准备
- 注册中心
- 认证中心和授权中心模块
- 认证中心配置
- 授权中心配置
- 验证码配置
- 资源服务器
- 测试
- 结尾
近排在搭一个Oauth+SpringSecurity的微服务框架,一来可以熟悉各个组件的使用,二来知道各组件如何搭配。
但是,这时候可能会有一些看官会说:“哎呀,网上不是很多这些已经搭建好了的框架吗?直接拿来就行啦,自己搭的话费时费力。”这就不对了啊,一名程序员要变强不仅仅要变秃,更要有钻研技术的能力哈。
环境准备
首先我这里发下目录结构:

bear-api模块:这里作为公共接口模块,主要给给他服务提供用户服务和日志服务。
bear-auth模块:认证中心和授权中心模块,用户在访问系统资源时需要在这里拿到授权后才能访问。
bear-common模块:公共模块,里面包括一些公共返回类、工具类、代码生成工具。
bear-gateway模块:网关模块,用的是Gateway作为网关中心。
bear-modules模块:资源模块,基本上系统的所有功能模块都在里面。
bear-server模块:注册中心,这里用的是Eureka。
注册中心
这没什么好说的,上一下我的配置文件:
server:
port: 8090
eureka:
instance:
instance-id: bear-server
hostname: 127.0.0.1
client:
fetch-registry: false
register-with-eureka: false
service-url:
default-zone: http://${eureka.instance.hostname}:${server.port}/erueka/
spring:
application:
name: bear-server
server:
port: 8090
eureka:
instance:
instance-id: bear-server
hostname: 127.0.0.1
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/erueka/
server:
enable-self-preservation: true
renewal-percent-threshold: 0.49认证中心和授权中心模块
引入maven依赖:
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.2</spring-cloud.version>
<jdbc.version>2.4.5</jdbc.version>
<kaptcha.version>2.3.2</kaptcha.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>pers.bear.framework.common.security</groupId>
<artifactId>common-security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>pers.bear.framework</groupId>
<artifactId>bear-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>pers.bear.framework.common</groupId>
<artifactId>common-core</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>${jdbc.version}</version>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
</dependencies>里面有两个包common-core和common-security的依赖,我待会再说。上一些配置文件:
server:
port: 8093
spring:
main:
allow-bean-definition-overriding: true
application:
name: bear-auth
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/bear_cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
hikari:
maximum-pool-size: 100
redis:
host: 127.0.0.1
port: 6379
eureka:
instance:
instance-id: bear-auth
lease-expiration-duration-in-seconds: 30
lease-renewal-interval-in-seconds: 20
client:
serviceUrl:
defaultZone: http://127.0.0.1:8090/eureka认证中心配置
首先我们来配置下认证中心:
/**
* @author Mr.Bear
* @date 2021/5/12 10:56
* 认证服务器
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private BearUserDetailService bearUserDetailService;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
DataSource dataSource;
/**
* 这里使用获取第三方客户端信息
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(){
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
// 重置查询sql语句
jdbcClientDetailsService.setSelectClientDetailsSql("select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from sys_oauth_client_details where client_id = ?");
return jdbcClientDetailsService;
}
/**
* 校验注册的第三方客户端的信息,可以存储在数据库中,默认方式是存储在内存中,如下所示,注释掉的代码即为内存中存储的方式
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.inMemory()
// .withClient("client")
// .secret("1234").scopes("all")
// .authorizedGrantTypes("authorization_code", "password", "refresh_token");
clients.withClientDetails(clientDetailsService());
}
/**
* 控制端点信息
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 使用默认DefaultTokenServices来创建令牌,令牌又随机值生成
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
// 设置过期时间
int expire = (int) (SecretContants.EXPIRE / 1000);
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.userDetailsService(bearUserDetailService);
defaultTokenServices.setTokenStore(endpoints.getTokenStore());
defaultTokenServices.setAccessTokenValiditySeconds(expire);
defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
endpoints.tokenServices(defaultTokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("permitAll()");
}
}因为我们是把第三方客户端存储在数据库中,所以需要spring-jdbc来进行获取,如果使用自定义表名的话就需要调用JdbcClientDetailsService中的setSelectClientDetailsSql来重写sql,下面给一下表设计和数据:
CREATE TABLE `sys_oauth_client_details` (
`client_id` varchar(48) 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 `sys_oauth_client_details` VALUES ('client_system', NULL, '$2a$10$kmGjSqUNgpJWEfmKHPlN4ek3qIv8UepGkziJgQ/YruqRotUNhotVi', 'server', 'password', NULL, NULL, 3600, 7200, NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;授权中心配置
接下来对SpringSecurity进行配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
BearUserDetailService bearUserDetailService;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 使用Bcrypt方式加密密码
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 以下请求皆忽略
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers("/js/**", "/css/**","/images/**");
}
/**
* 添加验证码校验
* @return
*/
@Bean
public AuthenticationProvider authenticationProvider(){
AuthenticationProvider authenticationProvider = new CodeProvider();
((CodeProvider) authenticationProvider).setPasswordEncoder(passwordEncoder());
((CodeProvider) authenticationProvider).setUserDetailsService(bearUserDetailService);
return authenticationProvider;
}
@Bean
public AuthenticationManager authenticationManager(){
ProviderManager providerManager = new ProviderManager(authenticationProvider());
return providerManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/**", "/code")
.permitAll()
.antMatchers(HttpMethod.POST, "/system/user", "/oper/log/insertLog")
.permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
}
}UserDetailService主要是通过执行**loadUserByUsername()**方法获取登录用户对应的信息,包括权限、账号密码等等,最后把这些信息都封装在UserDetails对象中,并与用户输入的密码进行匹配。实际开发中,基本都是自定义UserDetailService,所以我也自己实现loadUserByUsername方法
@Service("bearUserDetailService")
public class BearUserDetailService implements UserDetailsService {
@Autowired
RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 获取用户信息,我这里用的feign远程调用,根据自己的实现方式来获取用户信息就行了
RemoteUser remoteUser = remoteUserService.getUserInfoByUserName(s);
if (Objects.isNull(remoteUser)) {
throw new UsernameNotFoundException("用户不存在");
}
// 权限设置,把用户具有的权限标识和角色都放在 Set<SimpleGrantedAuthority>中
List<RemoteRole> remoteRoleList = remoteUser.getPermissions();
Set<SimpleGrantedAuthority> simpleGrantedAuthorities = new LinkedHashSet<>();
if (Objects.nonNull(remoteRoleList) && remoteRoleList.size() > 0) {
for (RemoteRole permission : remoteRoleList) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(permission.getRoleKey()));
}
}
return new LoginUser(remoteUser.getUserId(), remoteUser.getUserName(), remoteUser.getPassword(), true, true, true, true, simpleGrantedAuthorities, remoteUser);
}
}可以看到我返回的是一个LoginUser对象,我这里扩展了UserDetails对象,往其中加入了更多的信息,上下代码先:
@Data
public class LoginUser implements UserDetails {
// 用户ID
private Long userId;
// 用户名称
private String username;
// 用户密码
private String password;
// 用户对象信息
private RemoteUser remoteUser;
// 用户是否不过期
private boolean isAccountNonExpired;
// 用户是否不会被锁住
private boolean isAccountNonLocked;
// 密码是否不会过期
private boolean isCredentialsNonExpired;
// 这个我忘了
private boolean isEnabled;
// 是否管理员
private boolean isAdmin;
// 权限集合
private Set<SimpleGrantedAuthority> authorities;
public LoginUser(Long userId, String username, String password, boolean isAccountNonExpired, boolean isAccountNonLocked, boolean isCredentialsNonExpired, boolean isEnabled, Set<SimpleGrantedAuthority> authorities, RemoteUser remoteUser){
this.authorities = authorities;
this.username = username;
this.remoteUser = remoteUser;
this.userId = userId;
this.password = password;
this.isAccountNonExpired = isAccountNonExpired;
this.isAccountNonLocked = isAccountNonLocked;
this.isCredentialsNonExpired = isCredentialsNonExpired;
this.isEnabled = isEnabled;
this.isAdmin = false;
}
public LoginUser(Long userId, String username, String password, boolean isAccountNonExpired, boolean isAccountNonLocked, boolean isCredentialsNonExpired, boolean isEnabled, Set<SimpleGrantedAuthority> authorities, RemoteUser remoteUser, boolean isAdmin){
this.authorities = authorities;
this.username = username;
this.remoteUser = remoteUser;
this.userId = userId;
this.password = password;
this.isAccountNonExpired = isAccountNonExpired;
this.isAccountNonLocked = isAccountNonLocked;
this.isCredentialsNonExpired = isCredentialsNonExpired;
this.isEnabled = isEnabled;
this.isAdmin = isAdmin;
}
public LoginUser(String username, String password, Set<SimpleGrantedAuthority> authorities){
this.authorities = authorities;
this.username = username;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
}其中用户信息和是否管理员是我自己加上去的,各位看官可以根据实际情况加上自己需要的信息。
验证码配置
授权和认证都配置好了,为了安全起见,我们需要加到个验证码校验,实现方式如下:
/**
* @author Mr.Bear
* @date 2021/5/23 13:40
* 验证码配置
**/
@Configuration
public class KaptchaConfig {
@Bean
Producer kaptcha(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "5");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}验证码生成接口:
@RestController
public class CodeController {
@Autowired
Producer producer;
@GetMapping("/code")
public void getVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {
response.setContentType("image/jpeg");
String text = producer.createText();
session.setAttribute("code", text);
BufferedImage image = producer.createImage(text);
try(ServletOutputStream outputStream = response.getOutputStream()){
ImageIO.write(image, "jpg", outputStream);
}
}
}接下来需要新建一个provider将验证码的校验加入到springsercurity的校验流程中:
public class CodeProvider extends DaoAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String verifyCode = request.getParameter("code");
String sessionCode = (String) request.getSession().getAttribute("code");
if(Objects.nonNull(verifyCode) && Objects.nonNull(sessionCode) && sessionCode.toLowerCase().equals(verifyCode.toLowerCase())){
return super.authenticate(authentication);
}
throw new AuthenticationServiceException("验证码输入错误");
}
}最后上下Application的代码:
@SpringBootApplication
// 不用feign的话可以不用加
@EnableFeignClients(basePackages = {"pers.bear"})
@EnableEurekaClient
public class BearAuthApplication {
public static void main(String[] args) {
SpringApplication.run(BearAuthApplication.class, args);
}
}资源服务器
认证和授权服务器搭建好了,接下来我们配置下资源服务,因为有时候资源服务器可能很多,如果一个个配置的话就很麻烦了,我这里就把这个资源配置放到common-security模块中,资源需要保护的话直接引入该模块就行了。

这是ResourceOauthConfig的代码:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法权限注解
public class ResourceOauthConfig extends ResourceServerConfigurerAdapter {
@Autowired
RedisConnectionFactory redisConnectionFactory;
/**
* 使用redis存储token
* @return
*/
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/system/user/info")
.permitAll()
.antMatchers(HttpMethod.POST, "/system/user", "/oper/log/insertLog")
.permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}SecurityUtils工具类能够获取当前用户的信息。
public class SecurityUtils {
public static Authentication getAuthenticaion(){
return SecurityContextHolder.getContext().getAuthentication();
}
public static LoginUser getLoginUser(){
return (LoginUser) getAuthenticaion().getPrincipal();
}
public static RemoteUser getRemoteUser(){
return getLoginUser().getRemoteUser();
}
public static Long getUserId(){
return getLoginUser().getUserId();
}
}因为依赖引入的时候,Spring只会扫描到本目录的注解,其它模块的注解是扫描不到的,因此需要在spring.factories中加入以下一段话:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
pers.bear.framework.common.security.config.ResourceOauthConfig接下来我们需要用bear-system模块来进行实验:

配置文件跟认证中心的配置文件差不多,就改下instance-id这些地方,我这里就不上了。上下测试controller:
@RestController
@RequestMapping("/system/user")
public class UserController {
@Autowired
IUserService userService;
@GetMapping("list")
public Response list(){
List<User> userList = userService.list();
return Response.success(userList);
}application的代码
@SpringBootApplication
@EnableEurekaClient
// 开启资源服务器
@EnableResourceServer
public class BearSystemApplication {
public static void main(String[] args) {
SpringApplication.run(BearSystemApplication.class, args);
}
}到这里基本上完成搭建。
测试
到了最激动人心的测试环节了。我用postman来测试。首先获取一下验证码:

然后请求/oauth/token来获取访问token

access_token就是我们想要的,接下来携带这个token去请求我们想访问的接口:

访问成功,如果不带token的话就会出现401错误:

结尾
到这里整个流程基本完成了,各位有什么疑问的话可以在评论区反映,毕竟我写的也不是最好的解决方案哈哈。
















