文章目录
- Spring-security介绍
- Spring-security认证授权流程
- 认证流程
- Security流程
- 认证过滤器实现
- 获取UserDetail信息
- 配置Security
Spring-security介绍
Spring Security是一个功能强大且高度可定制的Java安全框架,用于保护基于Spring的应用程序。它提供了全面的安全服务,包括认证(Authentication)、授权(Authorization)、防止CSRF等。
- 认证(Authentication)
认证是确认用户身份的过程。Spring Security支持多种认证机制,如表单登录、HTTP基本认证、OAuth2、LDAP等。 - 授权(Authorization)
授权是确定用户是否有权限访问特定资源的过程。Spring Security提供了基于角色的访问控制(RBAC)和基于属性的访问控制(ABAC)。 - 防止CSRF攻击
Spring Security自动保护你的应用程序免受跨站请求伪造(CSRF)攻击。 - 会话管理
提供会话固定攻击的保护,并支持会话超时和并发会话控制。 - 输入验证
防止常见的安全漏洞,如SQL注入和跨站脚本(XSS)。 - 方法级安全性
使用Spring AOP,你可以在方法级别上实现安全性控制,例如,只有具有特定角色的用户才能访问特定的方法。 - 异常处理
提供了一个异常处理机制,允许你自定义安全相关的异常处理。 - 集成Spring Boot
Spring Security与Spring Boot集成良好,提供了自动配置和简化的配置选项。 - 支持多种认证提供者
可以与各种认证提供者(如数据库、LDAP、OAuth2提供者等)集成。 - 自定义认证和授权
Spring Security允许你自定义认证和授权逻辑,以满足特定需求。 - 单点登录(SSO)
支持单点登录解决方案,允许用户使用一组凭据访问多个应用程序。 - 安全通信
支持HTTPS和其他安全通信协议,以保护数据传输。
Spring-security认证授权流程
认证流程
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证authenticate()的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
Security流程
实现步骤:
1.构建一个自定义的service接口,实现SpringSecurity的UserDetailService接口。
2.建一个service实现类,实现此loadUserByUsername方法。
3.调用登录的login接口,会经过authenticationManager.authenticate(authenticationToken)方法。此方法会调用loadUserByUsername方法。
4.方法内部做用户信息的查询,判断用户名和密码是否正确,这是第一道认证。
5.如果没有查到信息就抛出异常。
6.如果查到信息了再接着查用户的权限信息,返回权限信息到SysUser实体。
7.此实体实现了SpringSecurity自带的userDetail接口。实现了getAuthorities方法。
8.每次查询权限都会调用此方法。
9.查询到的权限,会被返回到login接口。进行后续操作。
10.如果认证通过,通过身份信息中的userid生产一个jwt。
11.把完整的用户信息作为value,token作为key存入redis。
认证过滤器实现
代码入口 通过继承UsernamePasswordAuthenticationFilter过滤器来实现认证。
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {private final AuthenticationManager authenticationManager;public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;super.setFilterProcessesUrl("/user/login");}/*** 设置登录方式的认证实体*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {log.info("<==============登录认证开始===============>");SysUser user;Authentication authenticate;try {//获取用户传送的信息user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);//参数校验if (user == null) {throw new BusinessException("登录信息不能为空");}if(!StringUtils.hasText(user.getLoginTabs())){throw new BusinessException("登录类型不能为空");}LoginEnum value = LoginEnum.getValue(user.getLoginTabs());if(value == null){throw new BusinessException("登录类型未匹配");}//登录认证不需要抽离 只依赖Security 鉴权 目前只有手机号和账密登录后续自行叠加switch (value){case SYS_LOG_TYPE_1:authenticate = authenticationManager.authenticate(new UserDetailAuthenticationToken(user));break;case SYS_LOG_TYPE_2:authenticate = authenticationManager.authenticate(new MobileCodeAuthenticationToken(user.getPhone(), user.getPhoneCode()));break;default:throw new BusinessException("登录类型错误");}return authenticate;} catch (Exception e) {log.error("登录认证失败:",e);throw new BadCredentialsException(e.getMessage());}}/**认证成功生成token并返回*/@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {log.info("<==============登录认证成功===============>");JWTResponseUtils.successfulAuthentication(request, response,chain, authResult);}/**认证失败,返回失败原因*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {log.info("<==============登录认证失败:{}===============>", failed.getMessage());JWTResponseUtils.unsuccessfulAuthentication(request, response, failed);}
获取UserDetail信息
*** 账号认证器处理器只监听UserDetailAuthenticationToken*/
@Slf4j
@SuppressWarnings("all")
public class UserDetailAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {private final UserDetailsService userDetailsService;private final RedisKeyUtil redisKeyUtil;private final PasswordEncoder passwordEncoder;public UserDetailAuthenticationProvider(UserDetailsService userDetailsService, RedisKeyUtil redisKeyUtil, PasswordEncoder passwordEncoder){this.userDetailsService = userDetailsService;this.redisKeyUtil = redisKeyUtil;this.passwordEncoder = passwordEncoder;}@Overrideprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {UserDetailAuthenticationToken tokenReq = (UserDetailAuthenticationToken) authentication;String loginElement = Constants.STATE_INVALID;try{//获取是否开启登录要素校验loginElement = redisKeyUtil.get(RedisEnums.REDIS_LOGIN_ELEMENT.getCode());log.info("查看登录要素入参: key = "+RedisEnums.REDIS_LOGIN_ELEMENT.getCode()+"; value = "+loginElement);}catch (Exception exception){log.error("查看登录要素出现异常,入参: key = "+RedisEnums.REDIS_LOGIN_ELEMENT.getCode()+"; exception: ",exception);}log.info("<==============账号认证处理器处理开始===============>");try {// 根据账号,获取登录人员信息UserDetails userDetails = userDetailsService.loadUserByUsername(tokenReq.getUsername());//储存用户信息必须是SecurityUtils类SecurityUtils securityUtils = (SecurityUtils)userDetails;if(Constants.STATE_EFFECTIVE.equals(loginElement)){if(!StringUtils.hasText(tokenReq.getPhone())){throw new BusinessException("手机号不能为空");}if(!StringUtils.hasText(tokenReq.getPhoneCode())){throw new BusinessException("验证码不能为空");}String redisPhoneCode = redisKeyUtil.get(RedisEnums.REDIS_LOGIN_KEY.getCode() + tokenReq.getPhone());if(!StringUtils.hasText(redisPhoneCode)){throw new BusinessException("验证码已过期,请重新获取验证码");}if(!tokenReq.getPhoneCode().equals(redisPhoneCode)){throw new BusinessException("验证码不正确,请重新填写");}if(!tokenReq.getPhone().equals(securityUtils.getPhone())){throw new BusinessException("用户手机号绑定不正确,请重新填写");}}boolean matches = passwordEncoder.matches(tokenReq.getPassword(), securityUtils.getPassword());if(!matches){throw new BusinessException("密码不正确,请重新填写");}UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());log.info("<==============账号认证处理器处理结束===============>");return usernamePasswordAuthenticationToken;} catch (BusinessException e) {log.error("账号登录出现可控异常:"+e.getMessage());throw new BadCredentialsException(e.getMessage());} catch (Exception e) {log.error("账号登录出现不可控异常:",e);throw new BadCredentialsException("账号登录认证异常");}}@Overrideprotected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {return null;}@Overridepublic boolean supports(Class<?> authentication) {return (UserDetailAuthenticationToken.class.isAssignableFrom(authentication));}}
配置Security
/*** Security配置*/
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 业务逻辑bean*/@Autowiredprivate SysUserService sysUserService;/*** redis操作工具类*/@Autowired(required = false)private RedisKeyUtil redisKeyUtil;/*** 密码方式为加密设置*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 跨域设置*/@BeanCorsConfigurationSource corsConfigurationSource() {final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());return source;}/*** 账号密码认证设置*/@Bean(name = "userDetailsService")public UserDetailsService userDetailsService() {return new UserDetailsServiceImpl(sysUserService);}/*** 手机验证码认证设置*/@Bean(name ="mobileUserDetailsServiceImpl")public UserDetailsService mobileUserDetailsServiceImpl() {return new MobileUserDetailsServiceImpl(sysUserService);}/*** 账号验证码认证提供者设置*/@Beanpublic UserDetailAuthenticationProvider userDetailAuthenticationProvider() {return new UserDetailAuthenticationProvider(userDetailsService(),redisKeyUtil,passwordEncoder());}/*** 手机验证码认证提供者设置*/@Beanpublic MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {return new MobileCodeAuthenticationProvider(mobileUserDetailsServiceImpl(),redisKeyUtil);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//自定义认证处理器//手机号认证auth.authenticationProvider(mobileCodeAuthenticationProvider());//账密认证auth.authenticationProvider(userDetailAuthenticationProvider());}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable().authorizeRequests().anyRequest().authenticated().and().addFilter(new JWTAuthenticationFilter(authenticationManager())).addFilter(new JWTAuthorizationFilter(authenticationManager()))// 不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint()).accessDeniedHandler(new JWTAccessDeniedHandler()).and().logout().logoutUrl("/user/logout").logoutSuccessHandler(new JWTLogoutSuccessHandler());}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/auth/**").antMatchers("/api/**").antMatchers("/ws/**").antMatchers("/export/**").antMatchers("/accBalChk/**").antMatchers("/monAcc/**").antMatchers("/doc.html", "/doc.html/**", "/webjars/**", "/v2/**", "/swagger-resources", "/swagger-resources/**", "/swagger-ui.html", "/swagger-ui.html/**");}}