Oauth2.0是什么不在赘述,本文主要介绍如何使用SpringSecurity Oauth2.0实现自定义的用户校验
1.鉴权中心服务
首先,列举一下我们需要用到的依赖,本文采用的是数据库保存用户信息redis保存token的方式。
pom依赖:
---- security依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.3.4.RELEASE</version> </dependency>
---- oauth2.0依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.4.RELEASE</version> </dependency>
----- redis依赖用于存储token
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.4.RELEASE</version> </dependency>
----- mybatisPlus用于操作数据库
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency>
------mysql 驱动 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
添加完引用后,我们需要配置Oauth鉴权中的两大服务:鉴权服务和资源服务
一、鉴权服务
鉴权服务中我们需要配置存储token用的redis,用于从DB中拉取Client信息的服务类,用于进行Token生产和维护的服务类,最后还要外露获取token的节点和进行token校验的节点
在开始以前首先我们要搞清SpringSecurity中的一些基本类的作用和概念,这里贴出一篇不错的文章: https://www.cnkirito.moe/spring-security-1/
1.首先我们要定义我们的用户实体类,必须要实现UserDetails接口,我们的实体类中仅仅简单的定义了用户名和密码,剩下的全部实现自UserDetails接口
public class User implements UserDetails { @TableId(type = IdType.AUTO) private int id; private String username; private String password; private boolean isEnabled; public int getId() { return id; } public void setId(int id) { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setEnabled(boolean enabled) { isEnabled = enabled; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.isEnabled; } }
数据库脚本:
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(50) NOT NULL, `password` varchar(255) NOT NULL, `is_account_non_expired` tinyint(4) NOT NULL, `is_account_non_locked` tinyint(4) DEFAULT NULL, `is_credentials_non_expired` tinyint(4) NOT NULL, `is_enabled` tinyint(4) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
2.自定义自己的用户查找服务类,必须集成UserDetaailsService接口且实现loadUserByUsername接口,届时spring框架会通过这个方法内的自定义逻辑找到你想要的用户
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user= userMapper.queryUserByUsername(username); if (user==null){ throw new UsernameNotFoundException("user no found"); } return user; } }
userMapper:就是一个非常基础的数据库查询语句
<mapper namespace="com.example.oauth2.mapper.UserMapper"> <select id="queryUserByUsername" resultType="com.example.oauth2.entity.User"> select * from t_user where user_name=#{username} </select> </mapper>
3.配置鉴权服务器,我们需要把redis和上面自定义的用户查询配置到鉴权服务里
Configuration //开启鉴权服务器 @EnableAuthorizationServer public class AuthoriztionServerConfiguration extends AuthorizationServerConfigurerAdapter { //全局SpringSecurity AuthenticationManager 需要在SpringSecurity中注入,下文会写到 @Autowired private AuthenticationManager authenticationManager; //redis工厂会自动配置 @Autowired private RedisConnectionFactory redisConnectionFactory; //数据源会自动配置 @Autowired private DataSource dataSource; //我们自定义的UserDetailsService实现类 @Autowired private UserDetailsService userDetailsService; //全局加密编码类 @Autowired private BCryptPasswordEncoder passwordEncoder; /** * 用来配置令牌端点的安全约束 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security //允许所有用户访问 /oauth/token节点来获取token .tokenKeyAccess("permitAll()") //允许已经鉴权通过的用户访问 /oauth/check_token节点 .checkTokenAccess("isAuthenticated()") // 允许表单认证 .allowFormAuthenticationForClients(); } /** * 定义客户端详细信息的配置器 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 从DB中拉取client信息 clients.withClientDetails(jdbcClientDetailsService()); } /** *配置鉴权服务终结点配置 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{ endpoints.authenticationManager(this.authenticationManager) //配置允许访问的http方式 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST) //用于配置自定义的用户校验服务 .userDetailsService(userDetailsService) //配置自定义的token保存地址 .tokenStore(tokenStore()) //用于配置自定义的token维护服务 .tokenServices(tokenServices()); } /** * 配置使用redis来存储token * @return */ @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); } /** * 配置通过数据的方式去读取client信息 * @return */ @Bean public ClientDetailsService jdbcClientDetailsService(){ JdbcClientDetailsService jdbcClientDetailsService=new JdbcClientDetailsService(dataSource); jdbcClientDetailsService.setPasswordEncoder(passwordEncoder); return jdbcClientDetailsService; } /** * 自定义token的生产和过期机制 * @return */ @Bean public DefaultTokenServices tokenServices(){ DefaultTokenServices defaultTokenServices=new DefaultTokenServices(); defaultTokenServices.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(4)); defaultTokenServices.setRefreshTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(2)); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore()); return defaultTokenServices; } /** * 指定全局加密方式 * @return */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
同时我们需要在数据库中建一张client的信息表SQL脚本:
CREATE TABLE `oauth_client_details` ( `client_id` varchar(256) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
完成以上步骤后鉴权服务器就配置完成了,记下来我们需要配置资源服务器
2.资源服务器
资源服务器配置相对简单,我们只需要暴露出可供用户使用的节点即可
@Configuration //开启资源服务 @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception{ http .authorizeRequests() //暴露出/oauth/**的所有接口 .antMatchers("/oauth/**") .permitAll() .anyRequest() .authenticated(); } }
配置完资源服务之后最关键的一步来了,我们需要自定义自己的用户校验
3.自定义用户校验
SpringSecurity中所有的校验都是通过AuthenticationProvider来实现的,所以我们需要实现自己的Provider注入到用于校验的Provider列表中去
@Component public class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //前面我们自己实现的读取用户信息的实现类 @Autowired private UserDetailsService userDetailsService; //全局加密方式 @Autowired private BCryptPasswordEncoder passwordEncoder; //对表单传入的用户信息和线程上下文中的用户进行比对判断是否是正确的用户 @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException { //数据库中的密码 String authPassword = userDetails.getPassword(); //上下文中的密码 String tokenPassword = (String) usernamePasswordAuthenticationToken.getCredentials(); boolean isPass = passwordEncoder.matches(tokenPassword, authPassword); if (isPass) { return; } throw new AuthenticationServiceException("password.wrong"); } //根据表单中传入的用户名从数据库中获取用户信息 @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException { UserDetails user = userDetailsService.loadUserByUsername(username); if (user == null) { throw new AuthenticationServiceException("未找到用户"); } return user; } }
把我们自定的provider注入进去
@Configuration public class GlobalAuthenticationConfig extends GlobalAuthenticationConfigurerAdapter { @Autowired private MyAuthenticationProvider authenticationProvider; @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider( authenticationProvider); } }
最后我们需要配置一下SpringSecurity
4.配置SpringSecurity
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationProvider authenticationProvider; /** * 注入用户信息服务 * @return 用户信息服务对象 */ @Bean @Override public UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(HttpMethod.OPTIONS,"/oauth/**") .permitAll() .anyRequest() .authenticated() .and() .csrf() .disable(); } }
然后配置文件中配好数据库和redis信息
server: port: 5000 spring: datasource: url: xxx username: xxx password: xxx driver-class-name: com.mysql.jdbc.Driver redis: host: xxx port: 6379 database: 7 password: xxx mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml configuration: map-underscore-to-camel-case: true logging: level: ROOT: debug
在启动之前我们需要先在数据库中添加一个client和一个用户
@SpringBootTest class Oauth2ApplicationTests { @Autowired JdbcClientDetailsService jdbcClientDetailsService; @Autowired private BCryptPasswordEncoder passwordEncoder; @Test void contextLoads() { //通过我们在鉴权服务中注入的JdbcClientDetailsService来插入一条Client信息 BaseClientDetails baseClientDetails = new BaseClientDetails(); baseClientDetails.setClientId("client"); baseClientDetails.setClientSecret(passwordEncoder.encode("secret")); List<String> grantTypes = new ArrayList<>(); grantTypes.add("password"); baseClientDetails.setAuthorizedGrantTypes(grantTypes); jdbcClientDetailsService.addClientDetails(baseClientDetails); //用指定的密码生成器搞一个密码一会手动填到库里这里就不写sql 了 String str = passwordEncoder.encode("666"); System.out.println("终于他妈的完事了");/**/ } }
注意给scope填个select上面忘记写了
启动项目访问获取通通token的接口,完美。