前言
在微服务中,一般都会有一个专门的认证服务,在这个认证服务上完成认证后就可以直接访问其他服务,这种效果仅使用SpringSecurity是比较难实现的,本文将使用SpringCloud OAuth2+JWT的方式来实现。
依赖
先导入相关依赖
<!--核心两个依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<!--其他依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
认证服务代码
OAuth2配置
OAuth2使用简化模式,并且开启自动授权。简化模式需要用户先通过SpringSecurity认证。后面我们会配置SpringSecurity认证通过后自动访问OAuth2授权地址,因为开启了自动授权,所以会直接返回token给我们。
@Configuration
@EnableAuthorizationServer // 配置授权服务。
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// 用来配置客户端详情服务
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 这里是第三方合作用户的客户id,秘钥的配置
// 使用in-memory存储
clients.inMemory()
// 客户端账号
.withClient("c1")
// 客户端密钥
.secret(new BCryptPasswordEncoder().encode("secret"))
// 客户端可访问的资源列表
.resourceIds("user")
// 授权类型,此处使用简化模式
.authorizedGrantTypes( "implicit","refresh_token")
// 客戶端允许的授权范围,暂时用不上
.scopes("all")
// 是否自动授权,简化模式在登录SpringSecurity后还需要进行一个授权。false跳转到授权页面,让用户点击授权,如果是true,相当于自动点击授权,就不跳转授权页面
.autoApprove(true)
// 加上验证回调地址,返回授权码信息,授权码信息会附加在url后面
.redirectUris("http://localhost:8083/auth/token.html");
// 如果有多个用户,可以继续配置
// .and().withClient()
}
/**
* 使用JWT令牌
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");//JWT密码
return converter;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
//token增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter()));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
defaultTokenServices.setAccessTokenValiditySeconds(43200);//令牌有效期12小时
defaultTokenServices.setRefreshTokenValiditySeconds(259200);//刷新令牌有效期3天
return defaultTokenServices;
}
@Autowired
// 认证管理
private AuthenticationManager authenticationManager;
@Override
// 用来配置令牌(token)的访问端点
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
// 令牌管理服务
.tokenServices(tokenServices())
.accessTokenConverter(accessTokenConverter())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);// 允许post提交
}
@Override
// 用来配置令牌端点的安全约束,拦截规则
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//实际测试这三个不配置也可以,具体作用未知
security
// 提供公有密匙的端点,如果你使用JWT令牌的话, 允许
.tokenKeyAccess("permitAll()")
// oauth/check_token:用于资源服务访问的令牌解析端点,允许
.checkTokenAccess("permitAll()")
// 表单认证,申请令牌
.allowFormAuthenticationForClients();
}
}
SpringSecurity配置
SpringSecurity的配置和普通的单体应用一样,这里使用Filter+Manage+Provider的方式做登录,网上比较多的是用UserDetailsService。但本文是三种登录方式,用Filter+Manage+Provider方便实现。简单说下这种方式,Filter负责拦截请求,然后调用Manage,Manage负责选择合适的Provider处理该登录请求。具体请看这篇文章 SpringSecurity 多种登录方式
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
//注入三个provider,对应密码,手机号,邮箱三种登录方式
@Bean
PasswordAuthenticationProvider passwordAuthenticationProvider(){
return new PasswordAuthenticationProvider();
}
@Bean
PhoneAuthenticationProvider phoneAuthenticationProvider(){
return new PhoneAuthenticationProvider();
}
@Bean
EmailAuthenticationProvider emailAuthenticationProvider(){
return new EmailAuthenticationProvider();
}
/**
* 配置过滤器
* @param authenticationManager
* @return
*/
AuthenticationProcessingFilter authenticationProcessingFilter(AuthenticationManager authenticationManager){
AuthenticationProcessingFilter userPasswordAuthenticationProcessingFilter = new AuthenticationProcessingFilter();
//为filter设置管理器
userPasswordAuthenticationProcessingFilter.setAuthenticationManager(authenticationManager);
//登录成功后跳转到OAuth2的认证地址
userPasswordAuthenticationProcessingFilter.setAuthenticationSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.sendRedirect("http://localhost:8083/auth/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://localhost:8083/auth/token.html");
});
//登录失败后跳转
userPasswordAuthenticationProcessingFilter.setAuthenticationFailureHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.sendRedirect("/login/failure");
});
return userPasswordAuthenticationProcessingFilter;
}
//配置管理器
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//往管理器中添加provider
auth.authenticationProvider(passwordAuthenticationProvider())
.authenticationProvider(phoneAuthenticationProvider())
.authenticationProvider(emailAuthenticationProvider());
}
// 认证管理器
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//注入密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
//return NoOpPasswordEncoder.getInstance(); // 不加密
return new BCryptPasswordEncoder();// BCryptPasswordEncoder加密
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
//配置拦截路径等信息
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().antMatchers("/user/**","/test/**").authenticated()
.and()
.formLogin().loginPage("/login/noLogin");//未登录跳转到此接口
/*.loginProcessingUrl("/process")
.successForwardUrl("/login/success")
.failureForwardUrl("/login/failure");*/
http.addFilterBefore(authenticationProcessingFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
}
资源服务器代码
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 配置JWT
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123"); // 对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources
// 资源id,与认证服务器对应,如果资源id不一致会认证失败
.resourceId("user")
// // 令牌服务
// .tokenServices(tokenService())
// 令牌服务
.tokenStore(tokenStore())
.stateless(true)
//认证异常
.authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
ObjectMapper objectMapper=new ObjectMapper();
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(R.error("认证异常")));
})
//无权访问
.accessDeniedHandler((httpServletRequest, httpServletResponse, e) -> {
ObjectMapper objectMapper=new ObjectMapper();
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(R.error("无权访问")));
});
}
/**
* 配置拦截路径、权限等
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
// 所有的访问,授权访问都要是all,和认证服务器的授权范围一一对应
//.access("#oauth2.hasScope('all')")
//去掉防跨域攻击
.and().csrf().disable();
//session管理
/*.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);*/
}
}