前段时间写了一篇Oauth2.0的基础,今天我们来进行项目实战
授权服务
首先来看授权服务,先引用对应的包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
- 配置Spring Security 新建类WebSecurityConfig 继承 WebSecurityConfigurerAdapter,并添加@Configuration @EnableWebSecurity注解,重写三个方法,代码如下:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
//内存存储
// auth
// .inMemoryAuthentication()
// .passwordEncoder(passwordEncoder())
// .withUser("user")
// .password(passwordEncoder().encode("user"))
// .roles("USER");
}
/**
* 配置了默认表单登陆以及禁用了 csrf 功能,并开启了httpBasic 认证
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登陆页/login并允许访问
.formLogin().permitAll()
// 登出页
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 其余所有请求全部需要鉴权认证
.and().authorizeRequests().anyRequest().authenticated()
// 由于使用的是JWT,我们这里不需要csrf
.and().csrf().disable();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
其中我们指定了通过UserServiceDetail来进行用户验证,再来看一下UserServiceDetail类:
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username", s);
User user = userMapper.selectOne(userQueryWrapper);
if (user == null) {
throw new RuntimeException("用户名或密码错误");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<Role> roles = userMapper.selectRoleByUserId(user.getId());
roles.forEach(role->{
authorities.add(new SimpleGrantedAuthority(role.getName()));
});
return new UserModel(user.getUserName(),user.getPassword(),authorities);
}
}
该类实现UserDetailsService接口,重写loadUserByUsername方法,返回对象为UserDetails,我们可以自己来实现UserDetails,本例我们通过UserModel来实现,loadUserByUsername方法我们实现了用户登录校验,校验成功后查询相关权限,封装为authorities
- 配置认证服务器,代码如下:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//从WebSecurityConfig加载
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
/**
* 配置客户端详细信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//从数据库读取
clients.jdbc(dataSource);
/*clients.inMemory()
//客户端ID
.withClient("zcs")
.secret(new BCryptPasswordEncoder().encode("zcs"))
//权限范围
.scopes("app")
.resourceIds()
//授权码模式
.authorizedGrantTypes("authorization_code")
.redirectUris("www.baidu.com");*/
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenStore(tokenStore());
}
/**
* 在令牌端点定义安全约束
* 允许表单验证,浏览器直接发送post请求即可获取tocken
* 这部分写这样就行
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
/**
* 非对称加密算法对token进行签名
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
// 导入证书
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
return converter;
}
/**
* 使用JWT令牌存储
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("zcs"));
}
}
configure(ClientDetailsServiceConfigurer clients)我们指定了从数据库读取,方便维护,对应oauth_client_details表,客户端验证时会直接从库中匹配验证
configure(AuthorizationServerEndpointsConfigurer endpoints)我们使用JWT来产生令牌,然后我们通过CustomJwtAccessTokenConverter完成JWt令牌增强,令牌中增加用户信息
/**
* 自定义添加额外token信息
*/
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
Map<String, Object> additionalInfo = new HashMap<>();
UserModel user = (UserModel)authentication.getPrincipal();
additionalInfo.put("USER",user);
defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
return super.enhance(defaultOAuth2AccessToken,authentication);
}
}
按照Oauth2规范,传递的JWT令牌必须加密,所以我们采用了非对称加密的方式,具体生成方式如下:
keytool -genkeypair -alias oauth2 -keyalg RSA -keypass mypass -keystore oauth2.jks -storepass mypass
服务器执行该命令会生成oauth.jks秘钥文件,我们存放在resource目录
受保护资源服务
再来搭建受保护资源服务,引入oauth和web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
继承ResourceServerConfigurerAdapter类,代码如下:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法注解方式来进行权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 声明了资源服务器的ID是userservice,声明了资源服务器的TokenStore是JWT
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("userservice").tokenStore(tokenStore());
}
/**
* 配置TokenStore
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置公钥
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey = null;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
}
converter.setVerifierKey(publicKey);
return converter;
}
/**
* 配置了除了/user路径之外的请求可以匿名访问
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll();
}
}
前面我们生成了密钥,所以这一块我们需要配置公钥进行解密,相关命令如下:
keytool -exportcert -file pub.crt -keystore oauth2.jks -alias oauth2
服务器执行该命令会产生公钥,我们粘贴在resource下的public.cert
- 创建Controller进行测试
访问 http://127.0.0.1:8080/oauth/authorize?client_id=zcs&response_type=code&redirect_uri=www.baidu.com
client_id和redirect_uri需要和数据库中对应,然后跳转到登录页面进行登录,用户名密码在user表登录成功后点击进行授权:
会返回授权码:
通过Postman进行请求: http://127.0.0.1:8080/oauth/token?code=MVPfJY&grant_type=authorization_code&redirect_uri=www.baidu.com&client_id=zcs&client_secret=zcs
返回如下:
拿token进行资源请求访问:
- 注意:
因为我们校验client_serect指定了加密,所以库中密码需要存储加密后的: