本文有oAuth2授权流程、授权服务配置、资源服务配置。
Security认证流程:
1、请求先到持久的SecurityContextPersistenceFilter,这个filter会先从SecurityContextRepository仓库中获取SecurityContext,若找到,则将SecurityContext设置到SecurityContextHolder中,那么在后面的代码中,直接从SecurityContextHolder中获取SecurityContext。
2、若没找到,则请求到UsernamepasswordAuthenticationFilter,这个filter将用户名与密码封装成一个Token,并调用AuthenticationManager做认证。
3、AuthenticationManager调用AuthenticationProvider做认证,AuthenticationProvider调用UserDetailsService加载数据库中的数据得到UserDetails对象。
4、AuthenticationProvider调用PasswordEncoder对比传入的密码和数据库中获取的密码是否一致。
5、校验成功后,将用户信息封装成UsernamePasswordAuthenticationToken对象,设置给SecurityContext,SecurityContext设置给SecurityContextHolder。
6、持久的SecurityContextPersistenceFilter从SecurityContextHolder中取出SecurityContext,存储到SecurityContextRepository中,并删除SecurityContextHolder中的数据。
7、请求再次到达时,执行第一步。
oAuth2授权流程:
1、授权服务执行流程
(1)获取授权码执行流程
1、执行登录时,会走Security认证流程,调用UserDetailsServer加载认证信息和权限列表;
2、获取授权码时,根据传入的ClientId,调用JdbcClientDetailsService获取数据库(表:oauth_client_details)中客户端的配置详情;
3、使用JdbcAuthorizationCodeServices生成授权码code,存储到数据库(表:oauth_code);
4、携带授权码,重定向到指定的url(表oauth_client_details中的web_server_redirect_uri字段指定的值);
(2)获取Token执行流程
1、请求携带授权码和一些参数到达,后台会先校验参数和授权码;
2、验证通过,根据用户的认证信息、授权信息、客户端详情信息生成一个Token,存储到内存;
3、返回Token给客户端;
2、资源服务授权流程
1、携带Token访问资源服务器;
2、资源服务器会携带Token到认证服务器发起请求验证Token;
3、检查Token,获取到Token背后的认证信息、权限信息,校验Token是否包含resourceId;
4、根据Token的权限列表,走Security方法授权;
授权服务配置
依赖:
<!--oauth2的包,包含了security包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- druid包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<!-- mysql 数据库驱动. -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plus包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--web包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1、Security的配置
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* security的配置
*/
@Configuration
@EnableWebSecurity //开启Spring的security
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码使用加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 密码授权模式
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 授权规则配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //定义权限规则
.antMatchers("/login").permitAll() //放行登录路径
// .antMatchers("/course/list").permitAll()
.anyRequest().authenticated() //其它所有路径都要认证才能访问
.and().formLogin() //允许表单登录
.successForwardUrl("/login/success") //登录成功跳转路径
.and().csrf().disable(); //关闭跨域伪造检查
}
}
2、授权服务器配置
写一个类MyAuthorizationServerConfigurerAdapter,继承AuthorizationServerConfigurerAdapter覆写三个configure方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
import java.util.Arrays;
/**
* 授权服务器配置器适配器
*/
@Configuration
@EnableAuthorizationServer //开启授权服务器配置
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
//访问数据库
@Autowired
private DataSource dataSource;
//密码加密模式
@Autowired
private PasswordEncoder passwordEncoder;
//1.客户端详情配置======================================================
/**
* 客户详情服务
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(){
//Jdbc客户端详细信息服务,操作数据库
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
//因为要对比数据库的信息,所以要设置密码加密的模式
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
/**
* 1.客户端详细信息服务配置
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/*//Jdbc客户端详细信息服务,操作数据库
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
//因为要对比数据库的信息,所以要设置密码加密的模式
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);*/
//客户详细信息
clients.withClientDetails(clientDetailsService());
}
//2.服务端点:授权码,令牌管理配置===================================
//认证管理器
@Autowired
private AuthenticationManager authenticationManager;
/**
* 定义 JdbcAuthorizationCodeServices 授权码的服务,基于数据库存储
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
JdbcAuthorizationCodeServices jdbcAuthorizationCodeServices = new JdbcAuthorizationCodeServices(dataSource);
return jdbcAuthorizationCodeServices;
}
/**
* 配置令牌的存储
* @return
*/
@Bean
public TokenStore tokenStore(){
//使用JWT令牌,不需要发请求
JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());
return jwtTokenStore;
//通过发请求访问的模式获取token
//return new InMemoryTokenStore();
}
/**
* Jwt访问令牌转换器
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
//Jwt访问令牌转换器
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置签名密钥
jwtAccessTokenConverter.setSigningKey(Oauth2Constant.SIGNINGKEY);
return jwtAccessTokenConverter;
}
/**
* 定义AuthorizationServerTokenServices ,令牌的服务配置
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenServices(){
//创建默认的令牌服务
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
//指定客户端详情配置
defaultTokenServices.setClientDetailsService(clientDetailsService());
//支持产生刷新token
defaultTokenServices.setSupportRefreshToken(true);
//设置token增强 - 设置token转换器
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain); //jwtAccessTokenConverter()
//token存储方式
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
/**
* 2.授权服务器端点配置器
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//密码授权模式需要
endpoints.authenticationManager(authenticationManager)
//授权码模式服务
.authorizationCodeServices(authorizationCodeServices())
//配置令牌管理服务
.tokenServices(tokenServices())
//允许post方式请求
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//3.授权服务安全配置,url是否放行等========================================
/**
* 3.授权服务器安全配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
checkTokenAccess("permitAll()") //对应/oauth/check_token 路径公开
.allowFormAuthenticationForClients(); //允许客户端进行表单身份验证,使用表单认证申请令牌
}
}
3、UserDetails对象
写一个类UserDetailsServiceImpl实现UserDetailsService接口,实现loadUserByUsername方法
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Repository
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private LoginMapper loginMapper;
@Autowired
private SystemFeignClient systemFeignClient;
/**
* 根据用户名查询数据,返回UserDetails对象
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询自己数据库中的用户信息
Login loginUser = loginMapper.selectByUsername(username);
if (loginUser == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if(loginUser.getType().intValue() == 0){ //如果是后台用户,就获取权限
//调用feign接口
AjaxResult ajaxResult = systemFeignClient.selectPermissionByLongId(loginUser.getId());
List<String> permissions = (List<String>) ajaxResult.getResultObj();
//权限转换为GrantedAuthority集合
grantedAuthorities = permissions.stream().map(permission ->
new SimpleGrantedAuthority(permission)
).collect(Collectors.toList());
}
//封装到UserDetails中(将用户对象转为json)
User user = new User(JSON.toJSONString(loginUser), loginUser.getPassword(), grantedAuthorities);
return user;
}
}
资源服务配置
依赖
<!--oauth2包,包含security包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
1、资源服务配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableResourceServer //开启资源服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法授权
public class MyResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
//1.1资源服务令牌验证服务,通过远程校验令牌
/*@Bean
public ResourceServerTokenServices resourceServerTokenServices(){
//使用远程服务请求授权服务器校验token , 即:资源服务和授权服务器不在一个主机
RemoteTokenServices services = new RemoteTokenServices();
//授权服务地址 , 当浏览器访问某个资源时就会调用该远程授权服务地址去校验token
//要求请求中必须携带token
services.setCheckTokenEndpointUrl("http://localhost:1100/oauth/check_token");
//客户端id,对应认证服务的客户端详情配置的clientId
services.setClientId("webapp");
//密钥,对应认证服务的客户端详情配置的client_secret
services.setClientSecret("123");
return services;
}*/
/**
* 资源服务器安全配置器
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//我的资源名称是什么
resources.resourceId("course");
//通过jwt方式解密私钥,不需要发送请求
resources.tokenStore(tokenStore());
//通过发请求的方式获取
//resources.tokenServices(resourceServerTokenServices());
}
/**
* 配置令牌的存储
* @return
*/
@Bean
public TokenStore tokenStore(){
//使用JWT令牌,不需要发请求
JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());
return jwtTokenStore;
//通过发请求访问的模式获取token
//return new InMemoryTokenStore();
}
/**
* Jwt访问令牌转换器
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
//Jwt访问令牌转换器
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//设置签名密钥
jwtAccessTokenConverter.setSigningKey(Oauth2Constant.SIGNINGKEY);
return jwtAccessTokenConverter;
}
/**
* Http安全性
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
//校验scope必须为hrm , 对应认证服务的客户端详情配置的clientId
.antMatchers("/**").access("#oauth2.hasScope('hrm')")
.and().csrf().disable(); //关闭跨域伪造检查
}
}
2、controller层方法授权
@PreAuthorize(“hasAuthority(‘course:online’)”) //course:online具体的权限。
需要资源服务配置类上开启方法授权:@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法授权
@RestController
@RequestMapping("/course")
public class CourseController {
@Autowired
public ICourseService courseService;
/**
* 课程上线
* @PreAuthorize("hasAuthority('course:online')") //方法授权
* @param courseIdd
* @return
*/
@PreAuthorize("hasAuthority('course:online')") //方法授权-------
@RequestMapping(value = "/onLineCourse/{id}", method = RequestMethod.POST)
public AjaxResult onLineCourse(@PathVariable("id") Long courseIdd) {
courseService.onLineCourse(courseIdd);
return new AjaxResult();
}
}