本文采用的springboot去整合springsecurity,采用oauth2.0授权认证,使用jwt对token增强。本文仅为学习记录,如有不足多谢提出。
OAuth 2.0是用于授权的行业标准协议。OAuth 2.0为简化客户端开发提供了特定的授权流,包括Web应用、桌面应用、移动端应用等。
org.springframework.boot spring-boot-starter-securityorg.springframework.security.oauth spring-security-oauth22.2.6.RELEASE #本文采用的springboot版本为2.6.3,由于Spring Security 在 Spring Boot 2.7.0 中已弃用的 WebSecurityConfigurerAdapter 所有在配置。所以在配置SpringSecurity配置时,原先configure采用bena配置SecurityFilterChain bean org.springframework.security spring-security-jwt1.1.0.RELEASE
package com.example.health.model; import com.alibaba.fastjson.annotation.JSONField; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Set; /** * 登录用户信息 */ @Data public class SecurityUser implements UserDetails { /** * 用户id */ private Long userId; /** * 用户名 */ private String username; /** * 部门ID */ private Long deptId; /** * 用户密码 */ private String password; /** * 用户状态 */ private Boolean enabled; /** * 权限数据 */ private Collectionauthorities; /** * 权限列表 */ private Set permissions; public SecurityUser() { } @Override public Collection extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } /** * 账户是否未过期,过期无法验证 */ @JSONField(serialize = false) @Override public boolean isAccountNonExpired() { return true; } /** * 指定用户是否解锁,锁定的用户无法进行身份验证 * * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 * * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 ,禁用的用户不能身份验证 * * @return */ @Override public boolean isEnabled() { return this.enabled; } }
package com.example.health.model.dto; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; /** * Oauth2获取Token返回信息封装 */ @Data @EqualsAndHashCode(callSuper = false) @Builder public class Oauth2TokenDto { /** * 访问令牌 */ private String token; /** * 刷新令牌 */ private String refreshToken; /** * 访问令牌头前缀 */ private String tokenHead; /** * 有效时间(秒) */ private int expiresIn; }
package com.example.health.security.handle; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.health.common.constant.MessageConstant; import com.example.health.mapper.SysUserMapper; import com.example.health.model.SecurityUser; import com.example.health.model.entity.SysUser; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; 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.Objects; @Service public class UserServiceImpl implements UserDetailsService { @Autowired private SysUserMapper sysUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper().lambda().eq(SysUser::getUserName, username)); if (Objects.isNull(sysUser)) { throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } SecurityUser securityUser = new SecurityUser(); BeanUtils.copyProperties(sysUser, securityUser); securityUser.setEnabled(!Objects.equals(0, sysUser.getStatus())); return securityUser; } }
/** * 认证失败处理类 返回未授权 * */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { int code = HttpStatus.UNAUTHORIZED; String msg = String.format("请求访问:%s,认证失败,无法访问系统资源", request.getRequestURI()); ServletUtils.renderString(response, JSON.toJSONString(ResultUtils.error(code, msg))); } }
/** * JWT内容增强器 */ @Component public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Mapinfo = new HashMap<>(); //把用户ID设置到JWT中 info.put("user_id", securityUser.getUserId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
/** * 认证服务配置 */ @AllArgsConstructor @Configuration @EnableAuthorizationServer public class Oauth2Config extends AuthorizationServerConfigurerAdapter { private PasswordEncoder passwordEncoder; private UserServiceImpl userDetailsService; /** * 该对象用来支持 password 模式 */ private AuthenticationManager authenticationManager; private JwtTokenEnhancer jwtTokenEnhancer; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client-app") .secret(passwordEncoder.encode("123456")) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); Listdelegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter()); enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器 endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) //配置加载用户信息的服务 .accessTokenConverter(accessTokenConverter()) .tokenEnhancer(enhancerChain); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } @Bean public KeyPair keyPair() { //从classpath下的证书中获取秘钥对 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } }
/** * SpringSecurity配置 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 禁用HTTP响应标头 .headers().cacheControl().disable().and() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage").permitAll() .antMatchers("/all/**").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
/** * 自定义Oauth2获取令牌接口 */ @RestController @RequestMapping("/oauth") public class AuthController { @Autowired private TokenEndpoint tokenEndpoint; /** * Oauth2登录认证 */ @RequestMapping(value = "/token", method = RequestMethod.POST) public BaseResponsepostAccessToken(Principal principal, @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException, HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody(); Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .token(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .tokenHead("Bearer ").build(); return ResultUtils.success(oauth2TokenDto); } }
@RestController @RequestMapping("/all") public class AllController { @GetMapping(value = "/getStr") public BaseResponse> getStr() { return ResultUtils.success(“All”); } }
@RestController @RequestMapping("/test") public class TestController { @GetMapping(value = "/getStr") public BaseResponse> getStr() { return ResultUtils.success("test"); } }