文章目录
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过滤器来实现认证。
java
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/user/login");
}
/**
* 设置登录方式的认证实体
*/
@Override
public 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并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
log.info("<==============登录认证成功===============>");
JWTResponseUtils.successfulAuthentication(request, response,chain, authResult);
}
/**
认证失败,返回失败原因
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
log.info("<==============登录认证失败:{}===============>", failed.getMessage());
JWTResponseUtils.unsuccessfulAuthentication(request, response, failed);
}
获取UserDetail信息
java
**
* 账号认证器处理器只监听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;
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {}
@Override
public 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("账号登录认证异常");
}
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return (UserDetailAuthenticationToken.class.isAssignableFrom(authentication));
}
}
配置Security
java
/**
* Security配置
*/
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 业务逻辑bean
*/
@Autowired
private SysUserService sysUserService;
/**
* redis操作工具类
*/
@Autowired(required = false)
private RedisKeyUtil redisKeyUtil;
/**
* 密码方式为加密设置
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 跨域设置
*/
@Bean
CorsConfigurationSource 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);
}
/**
* 账号验证码认证提供者设置
*/
@Bean
public UserDetailAuthenticationProvider userDetailAuthenticationProvider() {
return new UserDetailAuthenticationProvider(userDetailsService(),redisKeyUtil,passwordEncoder());
}
/**
* 手机验证码认证提供者设置
*/
@Bean
public MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {
return new MobileCodeAuthenticationProvider(mobileUserDetailsServiceImpl(),redisKeyUtil);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义认证处理器
//手机号认证
auth.authenticationProvider(mobileCodeAuthenticationProvider());
//账密认证
auth.authenticationProvider(userDetailAuthenticationProvider());
}
@Override
protected 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());
}
@Override
public 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/**");
}
}