本文使用Springsecurity、Oauth2实现单点登录功能,支持JWT,支持前后端分离。
【SSO】(SingleSignOn),就是通过用户的一次性鉴别登录。
【OAuth2】开放授权,是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。
【Springsecurity】 Spring 家族中的安全管理框架、集成Oauth2认证功能
以上详细概念请自行百度。
1. 使用架构
2. 架构图
主要实现 “授权服务器、资源服务器、自定义登录校验、JWT生成token” 等,闲话不多说,上代码:
1. Server端:授权服务器,登录校验
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.3.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
//这里要设置自定义认证
auth.authenticationProvider(loginValidateAuthenticationProvider);
}
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
// }
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout").permitAll()
.anyRequest().authenticated() // 其他地址的访问均需验证权限
.and()
.formLogin()
.loginPage("/login")
.and()
.logout().logoutSuccessUrl("/")
.and().csrf().disable().cors();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Component
public class LoginValidateAuthenticationProvider implements AuthenticationProvider {
@Autowired
private PermissionService permissionService;
@Autowired
private SysUserRepository sysUserRepository;
private static final String SPLIT_STR = "&&&";
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取输入的用户名
String username = authentication.getName();
//获取输入的明文
String rawPassword = (String) authentication.getCredentials();
//查询用户是否存在
SysUser sysUser = sysUserRepository.qryUser(username, Integer.parseInt(usernameSob[1]));
if (null == sysUser) {
throw new BadCredentialsException("用户不存在");
}
// 自定义用户信息
SysUserDto sysUserDto = new SysUserDto();
sysUserDto.setUsername(sysUser.getUsername());
sysUserDto.setRealName(sysUser.getRealName());
sysUserDto.setSob(usernameSob[1]);
String userJson = JSON.toJSONString(sysUserDto);
//验证密码
if (!Md5Util.isMatchPassword(rawPassword, sysUser.getPassword())) {
throw new BadCredentialsException("输入密码错误");
}
List permissionList = permissionService.findByAccount(sysUser.getUsername());
List authorityList = new ArrayList<>();
if (!CollectionUtils.isEmpty(permissionList)) {
for (SysPermission sysPermission : permissionList) {
authorityList.add(new SimpleGrantedAuthority(sysPermission.getCode()));
}
}
return new UsernamePasswordAuthenticationToken(userJson, rawPassword, authorityList);
}
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.accessTokenConverter(jwtAccessTokenConverter());
endpoints.tokenStore(jwtTokenStore());
}
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("zetor"); // Sets the JWT signing key
return jwtAccessTokenConverter;
}
}
2. Client端 :
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.3.RELEASE
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${exit_url}")
private String exit_url;
@Autowired
@Qualifier("resourceServerRequestMatcher")
private RequestMatcher resources;
@Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher nonResoures = new NegatedRequestMatcher(resources);
http.requestMatcher(nonResoures).authorizeRequests()
.anyRequest().authenticated()
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl(exit_url)
.and()
.cors()
.and()
.csrf().disable();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
//允许带凭证
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//对所有URL生效
source.registerCorsConfiguration("/**", config);
return source;
}
}
资源服务器(校验token)
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean("resourceServerRequestMatcher")
public RequestMatcher resources() {
return new AntPathRequestMatcher("/api/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(resources()).authorizeRequests()
.anyRequest().authenticated()
.and()
.cors()
.and()
.csrf().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.tokenServices(tokenServices());
}
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("zetor");
return converter;
}
/**
* resourceServerTokenServices 类的实例,用来实现令牌服务。
*
* @return
*/
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
return defaultTokenServices;
}
}
Client Main 启动调用
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
首页controller(此处支持前后端分离)
@RequestMapping("/")
public String index(Authentication authentication, Model model) {
OAuth2AuthenticationDetails detail = (OAuth2AuthenticationDetails) authentication.getDetails();
log.info("【登录成功】username:{}, sessonId:{}, {}", authentication.getPrincipal(), detail.getSessionId(), detail.getTokenValue());
if (front_flag) {
return "redirect:" + front_url + PaasConstant.PRE_FIX + detail.getTokenValue();
} else {
model.addAttribute("token", detail.getTokenValue());
return "index";
}
}
1. 启动mysql:创建数据库,运行脚本。
注:脚本参考文末代码
2. 启动程序. Oauth2ServerApp -> ClientApp
3. 调用客户端
在浏览器地址栏输入:http://127.0.0.1:8081/client
统一跳转到认证服务器 http://127.0.0.1:8086/sso,如图
注:账套为本文自定义信息,可酌情扩展,也可删除。
用户名:admin 、密码:123456
登录成功:
以上,登录成功。
注:如果使用前后端分离系统,请在配置中增加前端首页地址,如图
FRONT_URL: http://10.0.0.32:9527/#/
登录成功后,将token返回前端即可。
俗话说 “人生到处都是坑,前人栽树后人乘凉”, 这里说一下以下几处问题:
1. 登录页面,自定属性如何获取?
网上有很多实现的例子,比如使用自定义校验类继承 WebAuthenticationDetails 等...... 但是此类方法依赖架构较多,对代码侵入较大。
本文采用取巧方式,即采用与username拼串方式带回后台,处理较简单,如上面图中的账套信息。
2. 客户端如何实现jwt校验?
Client如何校验自己的token有效性,此处要转换一下概念,客户端提供的接口,也是一种资源,所以要将Client端当做resourceServer来处理,这样一切就合理了。
3. WebSecurityConfigurerAdapter与ResourceServerConfigurerAdapter过滤优先级?
此问题网上回答较多,比如改order(100)顺序等,但实现并不完善。
本文采用国外网友的做法对两个拦截器过滤进行互斥处理,参考如下
@Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher nonResoures = new NegatedRequestMatcher(resources);
http.requestMatcher(nonResoures).authorizeRequests()
.anyRequest().authenticated()
详细处理方式,可参考文末源码。
后续文章会进阶介绍 handler处理等功能,请移步《第二篇》。
本文源码地址:
ym-paas-sso-oauth2: 基于SpringSecurity、oauth2实现sso单点登录系统。
下载代码的朋友点下star,多谢支持