上一篇文章主要讲解Spring Security基本原理,本文主要讲如何配置使用Spring Security,包括
- OAuth2.0认证配置
- 自定义登录页面实现与配置
- JWT token生成配置
基本介绍
pom引用
需要加入spring security和spring security oauth2的依赖
org.springframework.boot
spring-boot-starter-security
org.springframework.cloud
spring-cloud-starter-oauth2
OAuth2.0认证配置
WebSecurityConfig配置
- 首先进行登录页面相关配置
- 配置自定义登录页面为index.html,后端用户名及密码验证为"/login"
- 因为为自定义登录页面,配置"/index.html", "/login", "/resources/**", "/static/"不需要认证,其它请求需要认证。否则登录页面无法打开
- 配置退出登录时,删除session和cookie,并自定义退出成功后的handler
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 默认支持./login实现authorization_code认证
http
.formLogin().loginPage("/index.html").loginProcessingUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/index.html", "/login", "/resources/**", "/static/**").permitAll()
.anyRequest() // 任何请求
.authenticated()// 都需要身份认证
.and()
.logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").logoutSuccessHandler(customLogoutSuccessHandler).permitAll()
.and()
.csrf().disable();
}
- 加入自定义的
UsernamePasswordAuthenticationProvider
,用于实现用户名和密码认证
@Override
public void configure(AuthenticationManagerBuilder auth) {
// 定义认证的provider用于实现用户名和密码认证
auth.authenticationProvider(new UsernamePasswordAuthenticationProvider(usernamePasswordUserDetailService));
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- 实现
UsernamePasswordAuthenticationProvider
实现authenticate和supports方法用于用户认证
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
// 自定义实现的user detail,使用了用户名和密码验证,用户信息的获取
private UsernamePasswordUserDetailService userDetailService;
public UsernamePasswordAuthenticationProvider(UsernamePasswordUserDetailService userDetailService) {
this.userDetailService = userDetailService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 验证用户名和密码
if (userDetailService.verifyCredential(username, password)) {
List authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("user"));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password, authorities);
//获取用户信息
token.setDetails(userDetailService.getUserDetail(username));
return token;
}
return null;
}
@Override
public boolean supports(Class aClass) {
// 用户名和密码登录时,使用该provider认证
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass));
}
}
-
将自定义的登录页面编译后拷贝到resource/public文件下,登录页面为index.html。可以自行实现自己版本的登录页面,也可以参考文末awesome-admin中login-ui源码链接。
登录页面
JWT Token配置
Oauth2AuthorizationConfig配置
- 配置Oauth2 Client,本文通过ClientDetailsService实现Oauth2 Client.
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
ClientDetailsService clientDetailsService;
....
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
}
ClientDetailsService继承ClientDetailsService,正常应当从数据库中读取,为了演示方便,本文直接在代码中设置。客户端信息包括appId, appSecret, grantTypes, accessExpireTime, refreshExpireTime, redirectUrl, scopes等OAuth2协议中Client信息。
@Service
@Primary
public class ClientDetailsServiceImpl implements ClientDetailsService {
@Override
public ClientDetails loadClientByClientId(String s) {
if ("weixin".equals(s) || "app".equals(s) || "mini_app".equals(s) || "global".equals(s)) {
AppCredential credential = new AppCredential();
credential.setAppId(s);
credential.setAppSecret("testpassword");
credential.setGrantTypes("authorization_code,client_credentials,password,refresh_token,mini_app");
credential.setAccessExpireTime(3600 * 24);
credential.setRefreshExpireTime(3600 * 24 * 30);
credential.setRedirectUrl("http://localhost:3006,http://localhost:3006/auth,http://localhost:8000,http://localhost:8000/auth");
credential.setScopes("all");
return new AppCredentialDetail(credential);
}
return null;
}
}
- 配置JWT Token服务,用于生成token。配置性的内容,生产环境从来没变过,建议直接拷贝
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
...
@Autowired
TokenServiceFactory tokenServiceFactory;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenServices(tokenServiceFactory.JwtTokenService())
.authenticationManager(authenticationManager);
}
}
本文通过实现TokenServiceFactory实现Token Service,用于生成token,这部分代码比较琐碎,建议直接运用copy&paste技术(因为生产使用一年了,就没动过)。核心注意3点:
- 实现TokenEnhancer用于在token中加入一些自定义的信息
- 实现TokenConverter用于将哪些字段放入token
- 在accessTokenConverter方法中设置token生成的公钥和密钥
@Service
public class TokenServiceFactory {
private TokenKeyConfig tokenKeyConfig;
private ClientDetailsService clientDetailsService;
@Autowired
public TokenServiceFactory(
TokenKeyConfig tokenKeyConfig,
ClientDetailsService clientDetailsService) {
this.tokenKeyConfig = tokenKeyConfig;
this.clientDetailsService = clientDetailsService;
}
@Bean
@Primary
public AuthorizationServerTokenServices JwtTokenService() {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
// 设置自定义的TokenEnhancer, TokenConverter,用于在token中增加自定义的内容
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
return defaultTokenService(tokenEnhancerChain);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter(new CustomAccessTokenConverter());
// 设置token生成的公钥和密钥,密钥放在resource目录下
final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource(tokenKeyConfig.getPath()), tokenKeyConfig.getPassword().toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair(tokenKeyConfig.getAlias()));
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public org.springframework.security.oauth2.provider.token.TokenEnhancer tokenEnhancer() {
return new TokenEnhancer();
}
private AuthorizationServerTokenServices defaultTokenService(TokenEnhancerChain tokenEnhancerChain) {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
defaultTokenServices.setClientDetailsService(clientDetailsService);
return defaultTokenServices;
}
}
密钥生成参考:https://www.jianshu.com/p/c9d5a2aa8648
TokenEnhancer用于在token中加入自定义的内容,通常需要自己实现,会经常随着需求变动修改
public class TokenEnhancer implements org.springframework.security.oauth2.provider.token.TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map additionalInfo = new HashMap<>(2);
Authentication userAuthentication = authentication.getUserAuthentication();
// 如果是client认证,通常是服务间调取认证,token中加入admin角色
if (authentication.isClientOnly()) {
List authorities = new ArrayList<>(1);
authorities.add("admin");
additionalInfo.put("authorities", authorities);
} else {
// 如果是用户认证,token中加入user detail,验证用户名和密码时设置的user detail
additionalInfo.put("userInfo", userAuthentication.getDetails());
// 将authorities转换为string类型,便于json序列化
Set rolesInfo = new HashSet<>(userAuthentication.getAuthorities());
additionalInfo.put("authorities", rolesInfo.stream().map(auth -> auth.getAuthority()).toArray());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
CustomAccessTokenConverter通常是把所有claims放到token 中
@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
}
ResourceServerConfig配置
Spring Security除了作为认证服务器,本身还是资源服务器。本项目Spring Security支持自定义的登陆页面还有一些API接口,因此需要配置ResourceServerConfig
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/api/**", "/users/**", "/static/**")
.and()
.authorizeRequests()
.antMatchers("/api/**", "/users/**").authenticated();
}
}
Application注解
@SpringBootApplication
@EnableResourceServer //作为resource server
@EnableGlobalMethodSecurity(prePostEnabled = true) //允许在方法前加注解确定权限@PreAuthorize("hasAnyAuthority('admin',)")
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
效果
输入登录地址,因为涉及到前后端的配合及通过code换取token的转换,本项目实现了一个前端项目直接使用Spring Security及其登录页面,登录成功后跳回到前端首页,参考文末源码admin-ui。
-
登录页面输入localhost:8000然后会跳转到auth服务的登录页面如下图,用户名可以输入admin/admin实际上可以随便输入,因为上面认证代码实质上没有检验密码
登录页面
-
登录后成功页面, JWT Token保存在cookie中。
2.png
如果想直接访问Spring Security中的登录页面,则启动你的Spring Security服务访问下面连接(假设服务端口为5000),也可以下载文末awesome-admin中的admin-service源码启动config、registry、auth服务。登录后可以成功跳转到redirect_uri并且生成了code,用此code可以通过postman获取token。
使用下面登录url, 输入用户名和密码均为admin后跳转的redirect_uri
http://localhost:5000/auth/oauth/authorize?response_type=code&client_id=app&redirect_uri=http://localhost:3006/auth-
跳转后可以看到连接中的code
登录成功后获取code -
以code为参数通过API获取token,注意要使用basic auth认证,用户名和密码就是你之前在
ClientDetailsService
中配置的app和appsecret
http://localhost:5000/auth/oauth/token?grant_type=authorization_code&code=pYIspF&redirect_uri=http://localhost:3006/auth
Code获取JWT Token API
面向Copy&Paste编程
- awesome-admin源码
https://gitee.com/awesome-engineer/awesome-admin