新项目要开始了,所以想要使用spring boot来搭建架构,半天的时间,网上查阅各种资料,踩了诸多坑,成功实现单点登录,在此记录一下。
踩坑日记请查看springboot2.0+oauth搭建SSO单点登录之踩坑日记
本文只介绍环境搭建以及详细代码的编写,如果想要详细了解oauth2,请参阅阮一峰的理解OAuth 2.0
使用最新的版本2.1.3.RELEASE进行搭建,pom文件
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security.oauth
spring-security-oauth2
2.3.3.RELEASE
org.apache.commons
commons-lang3
3.7
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
8.0.11
com.alibaba
druid
RELEASE
com.google.guava
guava
19.0
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.3
org.springframework.boot
spring-boot-starter-data-redis
环境搭建好之后,就要开始撸代码了(〜^㉨^)〜
server:
port: 9100
spring:
application:
name: ytsd-server
mvc:
throw-exception-if-no-handler-found: true
datasource:
url: jdbc:mysql://localhost:3306/scoremanager?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
username: admin
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
redis:
host: 127.0.0.1
port: 6379
password: 123456
security:
oauth2:
resource:
filter-order: 99
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
mybatis:
type-aliases-package: com.ytsd.server.**.entity
mapper-locations: classpath:com/ytsd/server/**/dao/*.xml
package com.ytsd.server.test.entity;
import com.google.common.collect.Sets;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @Author: ysz
* @Date: 2019-03-20 10:53
*/
@Data
public class User implements UserDetails {
private String id;
private String password;
private String username;
private String realName;
private Set authorities = Sets.newHashSet();
//权限列表
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return this.authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}
//获取密码
@Override
public String getPassword() {
return password;
}
//获取用户名
@Override
public String getUsername() {
return username;
}
//账户是否生效
@Override
public boolean isAccountNonExpired() {
return true;
}
//账户是否锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//凭证是否生效
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否激活
@Override
public boolean isEnabled() {
return true;
}
}
package com.ytsd.server.test.service;
import com.ytsd.server.test.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
/**
*
* @Author: ysz
* @Date: 2019-03-20 10:50
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user=userService.findUserByName(userName);
if(user==null){
throw new UsernameNotFoundException("user not found");
}
return user;
}
}
认证服务器AuthorizationServerConfiguration.java,在此配置clientId、secret、scope以及token过期时间。
package com.ytsd.server.configuration.oauth2;
import com.ytsd.server.configuration.oauth2.exception.OauthException;
import com.ytsd.server.test.entity.User;
import com.ytsd.server.test.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author: ysz
* @Date: 2019-03-20 11:23
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
public MyUserDetailsService userDetailsService;
@Autowired
public OauthException oauthExceptionTranslator;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public TokenStore getTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
if (accessToken instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
Map additionalInformation = new LinkedHashMap<>();
additionalInformation.put("expiration", accessToken.getExpiration().getTime());
User user = (User) authentication.getPrincipal();
additionalInformation.put("userName", user.getUsername());
token.setAdditionalInformation(additionalInformation);
}
return accessToken;
};
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenEnhancer(tokenEnhancer())
.exceptionTranslator(oauthExceptionTranslator)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenStore(getTokenStore());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")
.scopes("all")
.authorities("ROLE_APP")
.secret(passwordEncoder().encode("123456"))
.accessTokenValiditySeconds(60 * 30)
.refreshTokenValiditySeconds(60 * 60);
}
}
资源服务器ResourceServerConfiguration.java,它决定了哪些路径是需要认证的,比如我访问/captcha是不需要验证才可以访问到的,但是其他路径是需要认证后才可以访问。
package com.ytsd.server.configuration.oauth2;
import com.ytsd.server.configuration.security.CustomAccessDecisionManager;
import com.ytsd.server.configuration.security.CustomFilterInvocationSecurityMetadataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* @Author: ysz
* @Date: 2019-03-20 11:10
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "*";
@Autowired
CustomAccessDecisionManager decisionManager;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID);
}
@Bean
public FilterSecurityInterceptor filterSecurityInterceptor() {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setAccessDecisionManager(decisionManager);
filterSecurityInterceptor.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return filterSecurityInterceptor;
}
@Override
public void configure(HttpSecurity http) throws Exception{
http
.csrf().disable()
.exceptionHandling()
.and()
.authorizeRequests()
.antMatchers("/captcha/getCaptcha/*", "/captcha/checkCaptcha/*", "/druid/*")
.permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.disable();
}
}
package com.ytsd.server.configuration.security;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* @Author: ysz
* @Date: 2019-03-20 10:23
*/
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object obj, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) obj).getHttpRequest();
for (GrantedAuthority ga : authentication.getAuthorities()) {
String authority = ga.getAuthority();
String url = StringUtils.substringAfter(authority," ");
String method = StringUtils.substringBefore(authority," ");
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url,method);
if (matcher.matches(request)) {
return;
}
}
throw new AccessDeniedException("Access Denied");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
一般会重写,实现FilterInvocationSecurityMetadataSource接口,FilterInvocationSecurityMetadataSource接口有3个方法:
boolean supports(Class> clazz);
Collection
Collection
第一个方法不清楚其作用, 一般返回true
第二个方法是Spring容器启动时自动调用的, 返回所有权限的集合. 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里
第三个是当接收到一个http请求时, filterSecurityInterceptor会调用的方法. 参数object是一个包含url信息的HttpServletRequest实例. 这个方法要返回请求该url所需要的所有权限集合
package com.ytsd.server.configuration.security;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
/**
* @Author: ysz
* @Date: 2019-03-20 10:53
*/
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Override
public Collection getAllConfigAttributes() {
Collection co=new ArrayList<>();
co.add(new SecurityConfig("null"));
return co;
}
@Override
public Collection getAttributes(Object object) {
return null;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
网上查询到的很多资料都没有重写,但是在认证服务器中使用时报无法注入bean的异常,所有重写了一下
package com.ytsd.server.configuration.websecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author: ysz
* @Date: 2019-03-20 14:01
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
到这里基本就完成了,使用postman测试一下
正确获取到token,单点登录成功
如有哪里写的有问题,或有侵权,请留言告知!