概述:Spring OAuth2有四种授权方式:
1、授权码模式(authorization code)
2、简化模式(implicit)
3、密码模式(resource owner password credentials)
4、客户端模式(client credentials)
其中用得比较多的是密码模式和客户端模式,下面就举例客户端模式,本示例项目中认证服务和资源服务是同一服务
参考文章地址
一、前期准备
1.引入maven依赖
2.数据库脚本导入
-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` blob NULL,
`refresh_token` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '名称',
`password` varchar(255) NOT NULL DEFAULT '0' COMMENT '密码',
`role` varchar(255) NOT NULL COMMENT '角色',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
3.实体类和工具类
实体类:
/**
* User类,有账号和密码就行,其他的字段有需要再加
*/
public class ClUser implements Serializable {
private static final long serialVersionUID = -7797183521247423117L;
private Integer id;
private String userName;
private String password;
}
import lombok.Data;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
*用作UserDetailsService返回
*/
@Data
@ToString
public class UserInfo extends User {
private String id;
private String name;
private String companyId;
public UserInfo(String username, String password, Collection<? extends GrantedAuthority> authorities){
super(username,password,authorities);
}
}
工具类:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* MD5工具类
*/
public class MD5Util {
static MessageDigest MD5 = null;
static {
try {
MD5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException ne) {
ne.printStackTrace();
}
}
/**
* 获取md5值.
*
* @param str the string
* @return md5串
* @throws IOException
*/
public static String getStringMD5(String str) {
StringBuilder sb = new StringBuilder();
try {
byte[] data = str.getBytes("utf-8");
MessageDigest MD5 = MessageDigest.getInstance("MD5");
MD5.update(data);
data = MD5.digest();
for (int i = 0; i < data.length; i++) {
sb.append(HEX_DIGITS[(data[i] & 0xf0) >> 4] + "" + HEX_DIGITS[data[i] & 0xf]);
}
} catch (Exception e) {
}
return sb.toString();
}
}
二、配置认证服务
AuthorizationServerConfig 和 WebSecurityConfig 两个配置文件,这两个配置文件可以直接用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAuthorizationServer // 标识授权服务
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* webSecurityConfig 中配置的AuthenticationManager
*/
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
/**
* 此项目使用数据库保存 token 等信息所以要配置数据源
*/
@Autowired
private DataSource dataSource;
/**
* webSecurityConfig 中配置的 userDetailsService
*/
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* webSecurityConfig 中配置的 passwordEncoder(使用MD5加密)
*/
@Autowired
PasswordEncoder passwordEncoder;
@Bean
public TokenStore tokenStore() {
//使用内存中的 token store
// return new InMemoryTokenStore();
//使用Jdbctoken store
return new JdbcTokenStore(dataSource);
}
/**
* 对 oauth_client_details 表的一些操作
*
* @return ClientDetailsService
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Autowired
TokenStore tokenStore;
@Autowired
ClientDetailsService clientDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
// 请求token的时候会将client_id,client_secret等信息保存到 oauth_client_details 表中,所以需要手动创建该表
// 注意:以下注释的代码在请求了一次 token 之后则可以注释掉,否则如果不换 client 名字的话会因为主键冲突无法插入 client 信息。也可以一开始就注释,手动添加记录到数据库
// clients.jdbc(dataSource).withClient("client")
// .secret(passwordEncoder.encode("123456"))
// .authorizedGrantTypes("authorization_code", "refresh_token",
// "password", "implicit") // 四种认证模式
// .scopes("all")
// .authorities("ROLE_admin","ROLE_user")
// .redirectUris("http://www.baidu.com")
// .accessTokenValiditySeconds(120000)
// .refreshTokenValiditySeconds(50000);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
//允许check_token访问
.checkTokenAccess("permitAll()")
//允许表单登录
.allowFormAuthenticationForClients();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.tokenStore(tokenStore());
endpoints.userDetailsService(userDetailsService);
endpoints.setClientDetailsService(clientDetailsService);
//配置TokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// access_token 过期时间:5s
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1));
// refresh_token 过期时间,默认不过期
// tokenServices.setReuseRefreshToken(true);
// tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.SECONDS.toSeconds(20));
endpoints.tokenServices(tokenServices);
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore);
return tokenServices;
}
}
WebSecurityConfig:
import com.mind.chuangle.utility.MD5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* userDetailsService 获取token的时候对用户进行一些自定义过滤,并将保存用户信息(用户名,密码,角色等)
*/
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 使用MD5对client_secreat进行加密,可以使用默认的加密方式也可以自定义,这里使用MD5加密方式
*
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return MD5Util.getStringMD5(String.valueOf(charSequence));
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(MD5Util.getStringMD5(String.valueOf(charSequence)));
}
};
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置用户签名服务 主要是user-details 机制,
*
* @param auth 签名管理器构造器,用于构建用户具体权限控制
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll(); //新增login form支持用户登录及授权
}
}
三、实现UserDetailsService 接口
实现UserDetailsService 接口,查询用户信息
import com.mind.chuangle.common.exception.ExceptionCast;
import com.mind.chuangle.domain.ClUser;
import com.mind.chuangle.domain.UserInfo;
import com.mind.chuangle.domain.auth.response.AuthCode;
import com.mind.chuangle.utility.MD5Util;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.authority.AuthorityUtils;
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.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
protected final Log logger = LogFactory.getLog(getClass());
// @Autowired
// private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据名称去查数据库
ClUser clUser = new ClUser();
clUser.setUserName(username);
clUser.setPassword("123456");
// 权限列表
List<String> user_permission = new ArrayList<>();
String user_permission_string = StringUtils.join(user_permission.toArray(), ",");
user_permission.add("admin");
return new UserInfo(username, MD5Util.getStringMD5(clUser.getPassword()), AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
}
}
四、发起请求
注:在第一发起请求的时候 AuthorizationServerConfig中的注释要打开,往oauth_client_details插入一条数据
post请求地址:http://localhost:9999/oauth/token?username=liuzj&password=123456&grant_type=password&client_id=client&client_secret=123456
post刷新token:
http://localhost:9999/oauth/token?grant_type=refresh_token&refresh_token=2cfb3957-4d57-49fd-85ac-6689f011b966&client_id=client&client_secret=123456
五、配置资源服务
ResourceServerConfig 这个也可以直接用
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;
@Configuration
@EnableResourceServer // 标识是资源服务
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//禁用了csrf(跨站请求伪造)功能
.authorizeRequests()//限定签名成功的请求
//必须认证过后才可以访问;注意:hasAnyRole 会默认加上ROLE_前缀,而hasAuthority不会加前缀
.antMatchers("/decision/**","/govern/**").hasAnyRole("user") // 在角色过滤的时候需要注意user角色需要加角色前缀
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/test/**").authenticated()
// 免验证请求
.antMatchers("/oauth/**").permitAll();
}
}
六、测试是否拦截未认证的请求
这种未认证的结果其实可以捕获,封装为更容易接受的信息格式返回,此处暂不做优化,后续有时间再补
带上token访问接口