搭建一个oauth2服务器,包括认证、受权和资源服务器html
参考资料:前端
www.cnblogs.com/fp2952/p/89…java
Spring OAuth2官方文档github
本文分为两个部分web
项目地址:github.com/zheyday/Spr…算法
oauth分支spring
使用Spring Initializr新建项目,勾选以下三个选项数据库
pom.xml浏览器
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//只须要引用这一个
//集成了spring-security-oauth2 spring-security-jwt spring-security-oauth2-autoconfigure
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
复制代码
新建类WebSecurityConfig 继承 WebSecurityConfigurerAdapter,并添加@Configuration @EnableWebSecurity注解,重写三个方法,代码以下,详细讲解在代码下面
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
//内存存储
// auth
// .inMemoryAuthentication()
// .passwordEncoder(passwordEncoder())
// .withUser("user")
// .password(passwordEncoder().encode("user"))
// .roles("USER");
}
/** * 配置了默认表单登录以及禁用了 csrf 功能,并开启了httpBasic 认证 * * @param http * @throws Exception */
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登录页/login并容许访问
.formLogin().permitAll()
// 登出页
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 其他全部请求所有须要鉴权认证
.and().authorizeRequests().anyRequest().authenticated()
// 因为使用的是JWT,咱们这里不须要csrf
.and().csrf().disable();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
复制代码
主要讲解一下
protected void configure(AuthenticationManagerBuilder auth) throws Exception 复制代码
这个方法是用来验证用户信息的。将前端输入的用户名和密码与数据库匹配,若是有这个用户才能认证成功。咱们注入了一个UserServiceDetail
,这个service的功能就是验证。.passwordEncoder(passwordEncoder())
是使用加盐解密。
UserServiceDetail
实现了UserDetailsService
接口,因此须要实现惟一的方法
package zcs.oauthserver.service;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 zcs.oauthserver.model.UserModel;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceDetail implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE"));
return new UserModel("user","user",authorities);
}
}
复制代码
这里先用假参数实现功能,后面添加数据库
参数s是前端输入的用户名,经过该参数查找数据库,获取密码和角色权限,最后将这三个数据封装到UserDetails
接口的实现类中返回。这里封装的类可使用org.springframework.security.core.userdetails.User
或者本身实现UserDetails
接口。
UserModel
实现UserDetails
接口
package zcs.oauthserver.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.Collection;
import java.util.List;
public class UserModel implements UserDetails {
private String userName;
private String password;
private List<SimpleGrantedAuthority> authorities;
public UserModel(String userName, String password, List<SimpleGrantedAuthority> authorities) {
this.userName = userName;
this.password = new BCryptPasswordEncoder().encode(password);;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@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;
}
}
复制代码
新增username、password和authorities,最后一个存储的是该用户的权限列表,也就是用户拥有可以访问哪些资源的权限。密码加盐处理。
新建配置类AuthorizationServerConfig 继承 AuthorizationServerConfigurerAdapter,并添加@Configuration @EnableAuthorizationServer注解代表是一个认证服务器
重写三个函数
ClientDetailsServiceConfigurer
:用来配置客户端详情服务,客户端详情信息在这里进行初始化,你可以把客户端详情信息写死在这里或者是经过数据库来存储调取详情信息。客户端就是指第三方应用AuthorizationServerSecurityConfigurer
:用来配置令牌端点(Token Endpoint)的安全约束.AuthorizationServerEndpointsConfigurer
:用来配置受权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//从WebSecurityConfig加载
@Autowired
private AuthenticationManager authenticationManager;
//内存存储令牌
private TokenStore tokenStore = new InMemoryTokenStore();
/** * 配置客户端详细信息 * * @param clients * @throws Exception */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//客户端ID
.withClient("zcs")
.secret(new BCryptPasswordEncoder().encode("zcs"))
//权限范围
.scopes("app")
//受权码模式
.authorizedGrantTypes("authorization_code")
//随便写
.redirectUris("www.baidu.com");
// clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager);
}
/** * 在令牌端点定义安全约束 * 容许表单验证,浏览器直接发送post请求便可获取tocken * 这部分写这样就行 * @param security * @throws Exception */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
}
复制代码
客户端详细信息一样也是测试用,后续会加上数据库。令牌服务暂时是用内存存储,后续加上jwt。
先实现功能最重要,复杂的东西一步步往上加。
资源服务器也就是服务程序,是须要访问的服务器
新建ResourceServerConfig继承ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
// antMatcher表示只能处理/user的请求
.antMatcher("/user/**")
.authorizeRequests()
.antMatchers("/user/test1").permitAll()
.antMatchers("/user/test2").authenticated()
// .antMatchers("user/test2").hasRole("USER")
// .anyRequest().authenticated()
;
}
}
复制代码
ResourceServerConfigurerAdapter
的Order默认值是3,小于WebSecurityConfigurerAdapter
,值越小优先级越大
关于ResourceServerConfigurerAdapter
和WebSecurityConfigurerAdapter
的详细说明见
新建UserController
@RestController
public class UserController {
@GetMapping("/user/me")
public Principal user(Principal principal) {
return principal;
}
@GetMapping("/user/test1")
public String test() {
return "test1";
}
@GetMapping("/user/test2")
public String test2() {
return "test2";
}
}
复制代码
http://127.0.0.1:9120/oauth/authorize?client_id=zcs&response_type=code&redirect_uri=www.baidu.com
,而后跳出登录页面,地址栏会出现回调页面,而且带有code参数 http://127.0.0.1:9120/oauth/www.baidu.com?code=FGQ1jg
http://127.0.0.1:9120/oauth/token?code=FGQ1jg&grant_type=authorization_code&redirect_uri=www.baidu.com&client_id=zcs&client_secret=zcs
,code填写刚才获得的code,使用POST请求
有不少人会把JWT和OAuth2来做比较,其实它俩是彻底不一样的概念,没有可比性。
JWT是一种认证协议,提供一种用于发布接入令牌、并对发布的签名接入令牌进行验证的方法。
OAuth2是一种受权框架,提供一套详细的受权机制。
Spring Cloud OAuth2集成了JWT做为令牌管理,所以使用起来很方便
JwtAccessTokenConverter
是用来生成token的转换器,而token令牌默认是有签名的,且资源服务器须要验证这个签名。此处的加密及验签包括两种方式: 对称加密、非对称加密(公钥密钥) 对称加密须要受权服务器和资源服务器存储同一key值,而非对称加密可以使用密钥加密,暴露公钥给资源服务器验签,本文中使用非对称加密方式。
经过jdk工具生成jks证书,经过cmd进入jdk安装目录的bin下,运行命令
keytool -genkeypair -alias oauth2-keyalg RSA -keypass mypass -keystore oauth2.jks -storepass mypass
会在当前目录生成oauth2.jks文件,放入resource目录下。
maven默认不加载resource目录下的文件,因此须要在pom.xml中配置,在build下添加配置
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
复制代码
在原来的AuthorizationServerConfig中更改部分代码
@Autowired
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// endpoints.tokenStore(tokenStore)
// .authenticationManager(authenticationManager);
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenStore(tokenStore);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/** * 非对称加密算法对token进行签名 * @return */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
// 导入证书
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
return converter;
}
复制代码
jwtAccessTokenConverter
方法中有一个CustomJwtAccessTokenConverter
类,这是继承了JwtAccessTokenConverter
,自定义添加了额外的token信息
/** * 自定义添加额外token信息 */
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
Map<String, Object> additionalInfo = new HashMap<>();
UserModel user = (UserModel)authentication.getPrincipal();
additionalInfo.put("USER",user);
defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
return super.enhance(defaultOAuth2AccessToken,authentication);
}
}
复制代码
以前登录是用假数据,如今经过链接数据库进行验证。
创建三个表,user存储用户帐号和密码,role存储角色,user_role存储用户的角色
user表
role表
user_role表
使用mybatis-plus生成代码,改造以前的UserServiceDetail
和UserModel
UserServiceDetail
@Service
public class UserServiceDetail implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
@Autowired
public UserServiceDetail(UserMapper userMapper, RoleMapper roleMapper) {
this.userMapper = userMapper;
this.roleMapper = roleMapper;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username", s);
User user = userMapper.selectOne(userQueryWrapper);
if (user == null) {
throw new RuntimeException("用户名或密码错误");
}
user.setAuthorities(roleMapper.selectByUserId(user.getId()));
return user;
}
}
复制代码
经过UserMapper查询用户信息,而后封装到User中,在自动生成的User上实现UserDetails接口
User
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableId(value = "username")
private String username;
@TableId(value = "password")
private String password;
@TableField(exist = false)
private List<Role> authorities;
public User() {
}
public Integer getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = new BCryptPasswordEncoder().encode(password);
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username=" + username +
", password=" + password +
"}";
}
}
复制代码
解释说明:
UserDetails中须要重写一个方法,是存储用户权限的
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
复制代码
因此新增了一个变量,而且打上注解表示这不是一个字段属性
@TableField(exist = false)
private List<Role> authorities;
复制代码
在Role上实现GrantedAuthority接口,只须要权限名称就能够了
public class Role implements Serializable, GrantedAuthority {
private static final long serialVersionUID = 1L;
private String name;
@Override
public String toString() {
return name;
}
@Override
public String getAuthority() {
return name;
}
}
复制代码
在RoleMapper.java中新增方法,经过用户id查询拥有的角色
@Select("select name from role r INNER JOIN user_role ur on ur.user_id=1 and ur.role_id=r.id")
List<Role> selectByUserId(Integer id);
复制代码
测试方法和第一部分同样,获取令牌的时候返回以下
参考连接:
juejin.im/post/5c5ae6… 更多文章见我的博客 zheyday.github.io/