目录
1.2.4 自定义异常处理(http配置方法异常处理需要此配置)
单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。
一 认证授权业务层
1、用户登录服务
1.1 用户信息远程调用
1.1.1 业务描述
获取用户信息,基于feign方式建立远程服务调用。
1.1.2 业务逻辑代码
feign方式远程调用步骤:添加openfeign依赖-->启动类上添加 @EnableFeignClients注解 -->接口应用上添加 @FeignClient注解。
注意:@PathVariable注解内需要指定一个值。
参考代码:
启动类上添加:@EnableFeignClients注解
@SpringBootApplication()
@EnableFeignClients
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class,args);
}
}
服务接口上添加: @FeignClient注解
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(
value = "manage",
contextId = "RemoteUserService",
fallbackFactory = RemoteProviderFallbackFactory.class)
@RequestMapping("/user")
public interface RemoteUserService {
@GetMapping("/login/{username}")
User doSelectUserByUsername(@PathVariable("username") String username);
@GetMapping("/permission/{userId}")
List<String> doSelectUserPermissionByUserId(@PathVariable("userId") Integer UserId);
}
@FeignClient注解内参数说明:
①: value:调用的远程服务名。
②: contextId:bean名称。
@FeignClient描述的接口底层会为其创建实现类。默认会采用value的值作为bean的名称,为避免bean重名导致调用对象异常,指定一个contextId的值,一般使用接口应用的名称首字母小写作为contextId的值。
③: fallbackFactory:自定义异常处理对象。
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class RemoteProviderFallbackFactory implements FallbackFactory<RemoteProviderService> {
@Override
public RemoteProviderService create(Throwable throwable) {
log.error("服务调用失败:{}",throwable.getMessage());
return msg -> "服务忙,稍等片刻在访问";
}
}
1.2 用户信息业务处理
1.2.1 业务描述
获取登录用户信息,并进行封装。
1.2.2 业务逻辑代码
1、业务类实现UserDetailsService对象,重写loadUserByUsername()方法。
2、注入令牌加密对象 和 远程调用服务对象。
参考代码:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userinfo = remoteUserService.doSelectUserByUsername(username);
if (userinfo == null){
throw new UsernameNotFoundException("用户不存在");
}
List<String> userPermission = remoteUserService.doSelectUserPermissionByUserId(userinfo.getId());
log.info("userPermission:{}",userPermission);
return new org.springframework.security.core.userdetails.User(
username,userinfo.getPassword(),
AuthorityUtils.createAuthorityList(userPermission.toArray(new String[]{})));
}
}
2、认证授权配置类
2.1 令牌配置
2.1.1 定义令牌签名key
JWT令牌签名时使用的秘钥(可以理解为盐值加密中的盐)。
private static final String SIGNING_KEY="AUTH";
2.1.2 配置存储策略
存储策略有JDBC,Redis,JWT等方式,这里采用的是JWT方式存储。
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
2.1.3 配置令牌创建及验签方式
基于此对象创建的令牌信息会封装到Oauth2AccessToken类型的对象中 ,然后存储到TokenStore对象,外界需要时,会从tokenStore进行获取。
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
2.1.4 完整代码参考
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
public class TokenConfig {
private static final String SIGNING_KEY="AUTH";
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
}
2.2 安全配置
2.2.1 定义安全配置对象
继承 WebSecurityConfigurerAdapter类,重写里面的 configure(HttpSecurity http) 方法。
***这里放行所有资源的访问(后续可以基于选择对资源进行认证和放行)***
@Override
protected void configure(HttpSecurity http) throws Exception {
//释放相关请求
http.authorizeRequests().anyRequest().permitAll();
}
2.2.2 定义加密方式
底层默认采用的是UUID加密方式。这里采用 BCryptPasswordEncoder 对象方式,这是一种不可逆的加密方式,相对于UUID要更安全。
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
2.2.3 定义认证管理对象
这个对象负责完成用户信息的认证,注意定义AuthenticationManager对象把它交给spring容器管理时,要用 authenticationManagerBean() 带 bean的这个方法。
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
2.2.4 完整代码参考
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
2.3 授权配置
思路:为谁认证-->到哪认证-->如何认证
2.3.1 定义认证授权配置对象
①:添加 @EnableAuthorizationServer 注解,开启认证授权服务。
②:继承 AuthorizationServerConfigurerAdapter 对象,重写里面的配置方法,注入认证管理对象(AuthenticationManager)、创建令牌对象(JwtAccessTokenConverter)、令牌存储对象(TokenStore)、用户信息对象(UserDetailsService)、令牌加密方式(BCryptPasswordEncoder)。
注入对象时可以使用全参构造把对象封装到一起,省略每个对象上面都要写一遍@Autowired注解(构造方法上默认有@Autowired注解)。
@EnableAuthorizationServer //开启授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
private BCryptPasswordEncoder bCryptPasswordEncoder;
private AuthenticationManager authenticationManagerBean;
private UserDetailsService userDetailsServiceImpl;
private TokenStore jwtTokenStore;
private JwtAccessTokenConverter jwtAccessTokenConverter;
2.3.2 为谁认证(客户端信息配置)
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("gateway_client")
.secret(bCryptPasswordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password","refresh_token");
}
2.3.3 到哪认证(授权安全配置)
定义令牌要认证、检查的URL。(思考:1.我们输入了用户名和密码,会提交到哪里(URL)?提交到的这个路径是否需要认证? 2.令牌过期了,哪个路径可以帮我们重新生成一个令牌?)
配置:
①:定义(公开)要认证的url (permitAll() 是官方定义好的);
②:定义(公开)令牌检查的url;
③:允许直接通过表单方式由客户端提交认证。
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
2.3.4 如何认证(Oauth2认证方式配置)
1、Oauth2 定义了一套认证规范,例如:为谁发令牌,都发什么内容。。
配置:
①:由谁完成认证?(认证管理器)。
②:谁负责访问数据库?(需要两部分信息:a.来自客户端;b.来自数据库)。
③:支持对什么请求进行认证?(默认支持post方式)。
④:认证成功以后令牌如何生成和存储?(默认令牌生成是UUID.randomUUID(),并且存储方式是内存)。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManagerBean)
.userDetailsService(userDetailsServiceImpl) //谁来负责访问
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE)
.tokenServices(authorizationServerTokenServices());//令牌如何生成、如何存储
}
2、 构建一个AuthorizationServerTokenServices 类型的bean,返回一个封装好的DefaultTokenServices对象,此对象提供了创建,获取,刷新token的方法。将这个对象作为参数传给 tokenServices()方法。
配置:
①:设置令牌生成策略。
②:设置是否支持令牌刷新(访问令牌过期了,是否支持通过令牌刷新机制,延迟令牌有效期)。
③:设置令牌增强(默认令牌会比较简单,没有业务数据,就是简单的随机字符串,担现在希望使用JWT的方式)。
④:设置令牌访问有效期。
⑤:设置令牌刷新有效期。
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices(){
//构建TokenService对象(此对象提供了创建,获取,刷新token的方法)
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
defaultTokenServices.setTokenStore(jwtTokenStore);
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setAccessTokenValiditySeconds(3600);
defaultTokenServices.setRefreshTokenValiditySeconds(3600*24);
return defaultTokenServices;
}
2.3.5 完整参考代码
import lombok.AllArgsConstructor;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
@Configuration
@AllArgsConstructor
@EnableAuthorizationServer //开启授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
private BCryptPasswordEncoder bCryptPasswordEncoder;
private AuthenticationManager authenticationManagerBean;
private UserDetailsService userDetailsServiceImpl;
private TokenStore jwtTokenStore;
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("gateway_client")
.secret(bCryptPasswordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password","refresh_token");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManagerBean)
.userDetailsService(userDetailsServiceImpl) //谁来负责访问
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST,HttpMethod.PUT,HttpMethod.DELETE)
.tokenServices(authorizationServerTokenServices());//令牌如何生成、如何存储
}
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices(){
//构建TokenService对象(此对象提供了创建,获取,刷新token的方法)
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
defaultTokenServices.setTokenStore(jwtTokenStore);
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setAccessTokenValiditySeconds(3600);
defaultTokenServices.setRefreshTokenValiditySeconds(3600*24);
return defaultTokenServices;
}
}
二 资源服务业务层
1、资源配置类
1.1 令牌配置
此处令牌配置代码与认证授权业务层的代码相同,拷贝过来即可,代码如下:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
public class TokenConfig {
private static final String SIGNING_KEY="AUTH";
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
}
1.2 资源服务配置
1.2.1 构建资源服务配置对象
①:添加 @EnableResourceServer 注解,开启服务
②:添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,此注解表示启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解。
③:继承ResourceServerConfigurerAdapter类,resource和http方法
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
1.2.2 重写resource配置方法
通过tokenStore获取token解析器对象,基于此对象对token进行解析。
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//定义令牌生成策略,不要创建令牌,是要解析令牌
resources.tokenStore(tokenStore);
}
1.2.3 重写http配置方法
配置释放相关请求。此处没有进行黑白名单路径配置,可根据需求在自行添加其他配置。
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
//认证异常
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
//访问异常
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}
1.2.4 自定义异常处理(http配置方法异常处理需要此配置)
1、构建Json转换方法(此方法可以定义成一个工具类后期调用,这里直接写在了配置类里)。
private void writeJsonToClient(HttpServletResponse response,
Map<String, Object> map) throws IOException {
String jsonStr = new ObjectMapper().writeValueAsString(map);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(jsonStr);
writer.flush();
}
2、认证异常处理
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return (request, response, exception) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message","没有认证~~~");
writeJsonToClient(response,map);
};
}
3、访问异常处理
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, exception) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_FORBIDDEN);
map.put("message", "没有访问权限,请联系管理员");
writeJsonToClient(response,map);
};
}
2.5 完整参考代码
import org.codehaus.jackson.map.ObjectMapper;
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.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.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//定义令牌生成策略,不要创建令牌,是要解析令牌
resources.tokenStore(tokenStore);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return (request, response, exception) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message","没有认证~~~");
writeJsonToClient(response,map);
};
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, exception) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_FORBIDDEN);
map.put("message", "没有访问权限,请联系管理员");
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(HttpServletResponse response,
Map<String, Object> map) throws IOException {
String jsonStr = new ObjectMapper().writeValueAsString(map);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(jsonStr);
writer.flush();
}
}
2、授权业务处理
在Controller控制层方法上添加 @PreAuthorize 注解,并使用 hasAuthority() 方法,传入授权信息。
*注:在资源配置类上 添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,@PreAuthorize 注解才会生效。
参考代码
@PreAuthorize("hasAuthority('sys:user:status')")
@PutMapping("/updateStatus")
public SysResult updateStatus(@RequestBody User user){
userService.updateStatus(user);
return SysResult.success();
}