目录
pom引入
配置Spring Security
1.实现UserDetailsService接口
2.登录成功处理
3.登录失败处理
4.登出处理
5.没有权限处理设置
6.匿名用户访问处理
7.指定加密方式
8.WebSecurityConfig配置
oauth2处理
配置授权服务器
配置资源服务器
jwt配置
jwt转换器
jwt扩展存储
本文整合了前两篇文章,再结合Oauth2实现了单点登录的基本处理。
之前的文章:
SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客
SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理_zmgst的博客-CSDN博客
11
5.1.6.RELEASE
1.2.46
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
2.3.6.RELEASE
org.springframework.security
spring-security-jwt
1.0.9.RELEASE
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.21
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
com.github.axet
kaptcha
0.0.9
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
javax.activation
activation
1.1.1
com.alibaba
fastjson
${fastjson.version}
cn.hutool
hutool-all
5.3.3
org.apache.commons
commons-lang3
3.8.1
commons-codec
commons-codec
1.15
org.springframework.boot
spring-boot-configuration-processor
true
关于SpringSecruity的基本知识,可以参考这位博主的专栏博客
https://blog.csdn.net/qq_32867467/category_9047805.html?spm=1001.2014.3001.5482
一些统一返回封装,数据库表创建处理我就不再写了,上一篇文章里去粘贴吧,这里只做主要流程的梳理
SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysPermissionService sysPermissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(StringUtils.isEmpty(username)){
throw new UsernameNotFoundException("用户名不能为空");
}
/**
* 通过用户名称获取用户信息
*/
SysUser user=sysUserService.getUserDetails(username);
if(user==null){
throw new UsernameNotFoundException("账号不存在,请联系管理员!");
}
if(1!=user.getState()){
throw new RuntimeException("账户已被锁定,请联系管理员!");
}
List grantedAuthorities = new ArrayList<>();
//获取该用户所拥有的权限
List sysPermissions = sysPermissionService.selectListPermissionByUser(user.getId());
// 声明用户授权
sysPermissions.forEach(sysPermission -> {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
grantedAuthorities.add(grantedAuthority);
});
SecuritySysUser ssu=new SecuritySysUser(user.getAccound(),user.getPassword(),grantedAuthorities);
ssu.setSysuser(user);
return ssu;
}
}
SecuritySysUser为继承了UserDetails的实现类User,代码
@EqualsAndHashCode(callSuper = false) public class SecuritySysUser extends User { /** * 用户信息 */ private SysUser sysuser; /** * 权限信息 */ private ListroleList; /** * 构造方法 * @param username * @param password * @param authorities 用户权限列表 */ public SecuritySysUser(String username, String password, Collection extends GrantedAuthority> authorities) { super(username, password, true, true, true, true, authorities); } }
两个请求的sql
@Component
public class CustomizeAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
//进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
Map results = new HashMap<>();
//返回json数据
JsonResult result = ResultTool.success(results);
//处理编码方式,防止中文乱码的情况
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
CaptchaException为自定义异常,不需要的去掉即可
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回json数据
JsonResult result = null;
if (e instanceof CaptchaException) {
//验证码错误
result = ResultTool.fail(ResultCode.USER_CAPTCHA_ERROR);
} else if (e instanceof AccountExpiredException) {
//账号过期
result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//密码错误
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
}else{
//其他错误
result = ResultTool.fail(ResultCode.COMMON_FAIL);
}
//处理编码方式,防止中文乱码的情况
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
CaptchaException:
public class CaptchaException extends AuthenticationException { public CaptchaException(String msg) { super(msg); } }
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
JsonResult result = ResultTool.success();
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.NO_PERMISSION);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
在WebSecurityConfig配置文件添加
/**
* 指定加密方式
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
这里有些类可能还没有写到,在后面代码里,验证码过滤器的处理,想添加的可以参考之前博客:
SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理_zmgst的博客-CSDN博客
/**
* spring security 配置类
* @Author: zm
* @Description:
* @Date: 2022/4/22 13:48
*/
@Configuration
@EnableWebSecurity //开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
//@Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户登录操作
*/
@Autowired
private UserDetailsServiceImpl userDetailsService;
/**
* 匿名用户访问无权限资源时的异常
*/
@Autowired
private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
/**
* 登陆失败执行方法
*/
@Autowired
private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
/**
* 没有权限设置
*/
@Autowired
private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
/**
* 登出成功执行方法
*/
@Autowired
private CustomizeLogoutSuccessHandler logoutSuccessHandler;
/**
* 验证码过滤
*/
// @Autowired
// private CaptchaFilter captchaFilter;
/**
*登录成功执行方法
* @return
*/
@Bean
public CustomizeAuthenticationSuccessHandler loginSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
/**
* 指定加密方式
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* AuthenticationManager
*
* 如果不声明,会导致授权服务器无AuthenticationManager,
* 密码模式:而password方式无法获取token
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 用户名密码授权管理器
*
* @return DaoAuthenticationProvider
*/
@Bean
public UserNamePasswordAuthenticationProvider daoAuthenticationProvider() {
return new UserNamePasswordAuthenticationProvider(userDetailsService, passwordEncoder());
}
@Override
protected void configure(AuthenticationManagerBuilder auth){
auth.authenticationProvider(daoAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/sysUser/addUser").permitAll() // 允许post请求/add-user,而无需认证
.antMatchers("/sysUser/captcha").permitAll()//验证码放过
.antMatchers("/login/**").permitAll()//验证码放过
.antMatchers("/oauth/**").permitAll()//oauth2 请求路径放过
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated() // 有请求都需要验证
//登入
.and().formLogin().
permitAll()//允许所有用户
.successHandler(loginSuccessHandler()).//登录成功处理逻辑
failureHandler(authenticationFailureHandler).//登录失败处理逻辑
//登出
and().logout().
permitAll()//允许所有用户
.logoutSuccessHandler(logoutSuccessHandler)//登出成功处理逻辑
//异常处理(权限拒绝、登录失效等)
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
.accessDeniedHandler(customizeAccessDeniedHandler)
// 无状态session,不进行存储 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class);
//http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
}
UserNamePasswordAuthenticationProvider:
public class UserNamePasswordAuthenticationProvider extends DaoAuthenticationProvider {
/**
* 构造方法
*
* @param userDetailsService 用户信息服务
* @param passwordEncoder 密码工具
*/
public UserNamePasswordAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this(userDetailsService, passwordEncoder, null);
}
/**
* 构造方法
*
* @param userDetailsService 用户信息服务
* @param passwordEncoder 密码工具
* @param userDetailsPasswordService 修改密码服务
*/
public UserNamePasswordAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, UserDetailsPasswordService userDetailsPasswordService) {
super();
setUserDetailsService(userDetailsService);
setPasswordEncoder(passwordEncoder);
setUserDetailsPasswordService(userDetailsPasswordService);
}
/**
* 验证通过后,查询权限等信息
*
* @param principal principal
* @param authentication authentication
* @param user user
* @return org.springframework.security.core.Authentication
*/
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
Set authorities;
SecuritySysUser securityUser = (SecuritySysUser) user;
return super.createSuccessAuthentication(principal, authentication, securityUser);
}
}
Oauth2有4种授权类型,这里只展示一种常用的密码模式的使用
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 自定义token处理
*/
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* 配置认证客户端
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 配置client_id
.withClient("client")
// 配置client_secret
.secret(passwordEncoder.encode("123123"))
//配置访问token的有效期
.accessTokenValiditySeconds(36000)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//配置申请的权限范围
.scopes("all")
/**
* 配置grant_type,表示授权类型
*
* authorization_code:授权码模式
* implicit:简化模式
* password:密码模式
* client_credentials: 客户端模式
* refresh_token: 更新令牌
*/
.authorizedGrantTypes("password", "refresh_token");
}
/**
* 自定义授权服务配置
* 使用密码模式需要配置
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//增加转换链路,以增加自定义属性
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints.authenticationManager(authenticationManager)//使用密码模式需要配置
.userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)//支持GET,POST请求
.reuseRefreshTokens(false)//refresh_token是否重复使用
.tokenStore(tokenStore) // 配置存储令牌策略
// .accessTokenConverter(jwtAccessTokenConverter) // token转化器,我们转为了JWT
.tokenEnhancer(enhancerChain) ///配置自定义tokenEnhancer
; //支持GET,POST请求;
}
/**
* 自定义授权令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
/**
* @Author: zm
* @Description: 配置资源服务器
* @Date: 2022/4/26 13:26
*/
@Configuration
@EnableResourceServer
public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.formLogin().and()
.requestMatcher(requestMatcher())
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
}
/**
* @Author: zm
* @Description: jwt 配置文件
* @Date: 2022/4/27 11:15
*/
@Configuration
public class JwtTokenStoreConfig {
@Autowired
private CustomProperties customProperties;
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// 配置JWT使用的秘钥
accessTokenConverter.setSigningKey(customProperties.getPrivateKey());
return accessTokenConverter;
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
/**
* @Author: zm
* @Description: jwt token扩展存储
* @Date: 2022/4/27 11:40
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String name=authentication.getName();
SecuritySysUser userInfo = (SecuritySysUser) authentication.getPrincipal();
Map info = new HashMap<>();
info.put("enhance", "enhance info");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
CustomProperties:
/** * @Author: zm * @Description: 自定义变量 * @Date: 2022/4/27 11:11 */ @Setter @Getter @Component @ConfigurationProperties(prefix = "zm.jwt") public class CustomProperties { private long expire; private String secret; private String header; private String privateKey; }#application.yml配置文件 zm: jwt: header: Authorization expire: 604800 # 7天,s为单位 secret: abcdefghabcdefghabcdefghabcdefgh privateKey: 123123
到此处我们的所有配置已经完成了,然后我们再修改之前的登录成功处理器CustomizeAuthenticationSuccessHandler的onAuthenticationSuccess方法,如下:
@Autowired
private SysUserService sysUserService;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//更新用户表上次登录时间、更新人、更新时间等字段
User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser sysUser = sysUserService.getUserDetails(userDetails.getUsername());
// sysUser.setLastLoginTime(new Date());
sysUser.setUpdateDate(LocalDateTime.now());
sysUser.setUpdateBy(sysUser.getAccound());
sysUserService.update(sysUser);
//获取生成得token
String header=httpServletRequest.getHeader(Constant.AUTHORIZATION);
if(header==null || !header.startsWith("Basic ")){
throw new UnapprovedClientAuthenticationException("请求头中无client信息");
}
String[] tokens = this.extractAndDecodeHeader(header, httpServletRequest);
String clientId = tokens[0];
String clientSecret = tokens[1];
// 2. 通过 ClientDetailsService 获取 ClientDetails
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
// String secret=clientDetails.getClientSecret();
// clientSecret=passwordEncoder.encode(clientSecret);
TokenRequest tokenRequest = null;
// 3. 校验 ClientId和 ClientSecret的正确性
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clientId:" + clientId + "对应的信息不存在");
}
// else if (passwordEncoder.matches(clientSecret,clientDetails.getClientSecret())) {
// throw new UnapprovedClientAuthenticationException("clientSecret不正确");
// }
else {
// 4. 通过 TokenRequest构造器生成 TokenRequest
tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "custom");
}
// 5. 通过 TokenRequest的 createOAuth2Request方法获取 OAuth2Request
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
// 6. 通过 Authentication和 OAuth2Request构造出 OAuth2Authentication
OAuth2Authentication auth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
// 7. 通过 AuthorizationServerTokenServices 生成 OAuth2AccessToken
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(auth2Authentication);
//此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
//进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
Map results = new HashMap<>();
results.put(Constant.AUTHORIZATION,new ObjectMapper().writeValueAsString(token));
//返回json数据
JsonResult result = ResultTool.success(results);
//处理编码方式,防止中文乱码的情况
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(token));
}
private String[] extractAndDecodeHeader(String header, HttpServletRequest request) {
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException var7) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, StandardCharsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
} else {
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
Constant里的数据:
/** * 权限header */ public static final String AUTHORIZATION = "Authorization"; /** * token开头 */ public static final String BEARER_TYPE = "Bearer"; /** * token开头 */ public static final String ACCESS_TOKEN = "access_token";
参考博文:
微服务安全Spring Security OAuth2实战_沮丧的南瓜的博客-CSDN博客_微服务springsecurity
Spring Security OAuth2自定义Token获取方式 | MrBird
Spring Security Oauth2 JWT、第三方登录、单点登录讲解,并使用Oauth2.0结合微服务进行单点登录_YxinMiracle的博客-CSDN博客_oauth单点登录第三方接入
Spring Security + OAuth2.0 + JWT 实现单点登录_资深糖分大叔的博客-CSDN博客