Julywhj 2019-12-14
疯狂创客圈 Java 高并发【 亿级流量聊天室实战】实战系列 【博客园总入口 】
架构师成长+面试必备之 高并发基础书籍 【Netty Zookeeper Redis 高并发实战 】
疯狂创客圈 高并发 环境 视频,陆续上线:
小视频以及所需工具的百度网盘链接,请参见 疯狂创客圈 高并发社群 博客
在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,特别是在spring boot项目中,spring security已经默认集成和启动了。
Spring Security 默认为自动开启的,可见其重要性。
如果要关闭,需要在启动类加上,exclude ={SecurityAutoConfiguration} 的配置
@EnableEurekaClient @SpringBootApplication(scanBasePackages = { "com.crazymaker.springcloud.user", "com.crazymaker.springcloud.seckill.remote.fallback", "com.crazymaker.springcloud.standard" }, exclude = {SecurityAutoConfiguration.class})
或者
spring: autoconfigure: exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
一般不建议关闭。
spring security核心组件有:Userdetails 、Authentication,UserDetailsService、AuthenticationProvider、AuthenticationManager 下面分别介绍。
authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
接口有4个get方法,分别获取
Authorities, 填充的是用户角色信息。
Credentials,直译,证书。填充的是密码。
Details ,用户信息。
Principal 直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。
因此可以推断其实现类有这4个属性。
这几个方法作用如下:
getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)
getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。
isAuthenticated: 获取当前 Authentication 是否已认证。
setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
方法含义如下:
getAuthorites:获取用户权限,本质上是用户的角色信息。
getPassword: 获取密码。
getUserName: 获取用户名。
isAccountNonExpired: 账户是否过期。
isAccountNonLocked: 账户是否被锁定。
isCredentialsNonExpired: 密码是否过期。
isEnabled: 账户是否可用。
提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,他可以用来获取UserDetails。
package org.springframework.security.core.userdetails; public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。
在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException。
负责真正的验证。
当我们使用 authentication-provider 元素来定义一个 AuthenticationProvider 时,如果没有指定对应关联的 AuthenticationProvider 对象,Spring Security 默认会使用 DaoAuthenticationProvider。DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证的方式,我们可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。
实现了自己的 AuthenticationProvider 之后,我们可以在配置文件中这样配置来使用我们自己的 AuthenticationProvider。其中 myAuthenticationProvider 就是我们自己的 AuthenticationProvider 实现类对应的 bean。
AuthenticationProvider 接口如下:
package org.springframework.security.authentication; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; public interface AuthenticationProvider { Authentication authenticate(Authentication var1) throws AuthenticationException; boolean supports(Class<?> var1); }
authenticate 表示认证的动作。
supports 表示所支持的 Authentication类型。Authentication 包含很多子类,如果 AbstractAuthenticationToken 。
AbstractAuthenticationToken implements Authentication
还有,可以自定义 Authentication ,比如 本实例所使用的: JwtAuthenticationToken。
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。
AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。
组件比较多,但是如果主要流程理顺了,也比较简单。
搞定两个 AuthenticationProvider:
(1) 从数据库获取用户
首先通过 UserDetailsService 获取 UserDetails,然后 通过 UserDetailsService 装配 DaoAuthenticationProvider
(2) 完成用户的认证
实现一个自己的 JwtAuthenticationProvider,完成用户的认证
(3)定制一个过滤器
(4)完成所有组件的装配
首先通过 UserDetailsService 获取 UserDetails,然后 通过 UserDetailsService 装配 DaoAuthenticationProvider。
package com.crazymaker.springcloud.user.info.service.impl; @Slf4j @Service public class UserAuthService implements UserDetailsService { private PasswordEncoder passwordEncoder; public UserAuthService() { //默认使用 bcrypt, strength=10 this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); } private UserPO loadFromDB(String username) { if (null == userDao) { userDao = CustomAppContext.getBean(UserDao.class); } List<UserPO> list = userDao.findAllByLoginName(username); if (null == list || list.size() <= 0) { return null; } UserPO userPO = list.get(0); return userPO; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserPO userPO = loadFromDB(username); //将salt放到password字段返回 return User.builder() .username(userPO.getLoginName()) .password(userPO.getPassword()) // .password(SessionConstants.SALT) //BCrypt.gensalt(); 正式开发时可以调用该方法实时生成加密的salt // .password(SessionConstants.SALT) .authorities(SessionConstants.USER_INFO) .roles("USER") .build(); } }
在 SecurityConfiguration 配置类中加入如下内容:
@Bean("daoAuthenticationProvider") protected AuthenticationProvider daoAuthenticationProvider() throws Exception { //这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致 DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider(); daoProvider.setUserDetailsService(userDetailsService()); return daoProvider; } @Override protected UserDetailsService userDetailsService() { return new UserAuthService(); }
继承于 AuthenticationProvider,实现一个自己的 JwtAuthenticationProvider,完成用户的认证
package com.crazymaker.springcloud.standard.security.provider; //... public class JwtAuthenticationProvider implements AuthenticationProvider { private RedisOperationsSessionRepository sessionRepository; private CustomedSessionIdResolver httpSessionIdResolver; public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository, CustomedSessionIdResolver httpSessionIdResolver) { this.sessionRepository = sessionRepository; this.httpSessionIdResolver = httpSessionIdResolver; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { DecodedJWT jwt = ((JwtAuthenticationToken) authentication).getToken(); if (jwt.getExpiresAt().before(Calendar.getInstance().getTime())) { throw new NonceExpiredException("认证过期"); } String sid = jwt.getSubject(); String otoken = jwt.getToken(); Session session = null; try { session = sessionRepository.findById(sid); } catch (Exception e) { e.printStackTrace(); } if (null == session) { throw new NonceExpiredException("认证有误,请重新登录"); } String json = session.getAttribute(G_USER); if (StringUtils.isBlank(json)) { throw new NonceExpiredException("认证有误,请重新登录"); } UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class); if (null == userDTO) { throw new NonceExpiredException("认证有误"); } String password = userDTO.getPassword(); String username = userDTO.getLoginName(); UserDetails user = User.builder() .username(username) .password(password) .authorities(SessionConstants.USER_INFO) .build(); String encryptSalt = password; try { Algorithm algorithm = Algorithm.HMAC256(encryptSalt); JWTVerifier verifier = JWT.require(algorithm) .withSubject(sid).build(); verifier.verify(jwt.getToken()); } catch (Exception e) { throw new BadCredentialsException("JWT token verify fail", e); } JwtAuthenticationToken token = new JwtAuthenticationToken(user, jwt, user.getAuthorities()); return token; } @Override public boolean supports(Class<?> authentication) { return authentication.isAssignableFrom(JwtAuthenticationToken.class); } }
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。
@EnableWebSecurity() public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuthenticationProvider()) .authenticationProvider(jwtAuthenticationProvider()); } //.... }
搞得再多,如果不通过过滤器,将 AuthenticationManager 用起来,也是没有用的。
package com.crazymaker.springcloud.standard.security.filter; //..... public class JwtAuthenticationFilter extends OncePerRequestFilter { private RequestMatcher requiresAuthenticationRequestMatcher; private List<RequestMatcher> permissiveRequestMatchers; private AuthenticationManager authenticationManager; private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); //..... @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Authentication authResult = null; /** * 场景: 从 zuul 过来,直接带上session 头 */ if (StringUtils.isNotEmpty(request.getHeader(SessionConstants.SESSION_SEED))) { request.setAttribute(SessionConstants.SESSION_SEED, request.getHeader(SessionConstants.SESSION_SEED)); UserDetails userDetails = User.builder() .username(request.getHeader(SessionConstants.SESSION_SEED)) .password(request.getHeader(SessionConstants.SESSION_SEED)) .authorities(SessionConstants.USER_INFO) .build(); authResult = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); successfulAuthentication(request, response, filterChain, authResult); filterChain.doFilter(request, response); return; } /** * 正常场景: 单体微服务访问,或者从Zuul过来,没有带 session head */ if (!requiresAuthentication(request, response)) { filterChain.doFilter(request, response); return; } AuthenticationException failed = null; try { String token = getJwtToken(request); if (StringUtils.isNotBlank(token)) { JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token)); DecodedJWT jwt = authToken.getToken(); //将 AuthenticationManager 用起来 authResult = this.getAuthenticationManager().authenticate(authToken); UserDetails user = (UserDetails) authResult.getPrincipal(); request.setAttribute(SessionConstants.SESSION_SEED, jwt.getSubject()); } else { failed = new InsufficientAuthenticationException("请求头认证消息为空"); } } catch (JWTDecodeException e) { logger.error("JWT format error", e); failed = new InsufficientAuthenticationException("请求头认证消息格式错误", failed); } catch (InternalAuthenticationServiceException e) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); failed = e; } catch (AuthenticationException e) { // Authentication failed failed = e; } if (authResult != null) { successfulAuthentication(request, response, filterChain, authResult); } else if (!permissiveRequest(request)) { unsuccessfulAuthentication(request, response, failed); return; } filterChain.doFilter(request, response); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, failed); } //.... }
还是在 UserWebSecurityConfig 配置文件,将 HttpSecurity 的过滤机制配置起来,完成所有组件的装配。
代码如下:
package com.crazymaker.springcloud.user.info.config; //... @EnableWebSecurity() public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserAuthService userAuthService; protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers( "/v2/api-docs", "/swagger-resources/configuration/ui", "/swagger-resources", "/swagger-resources/configuration/security", "/swagger-ui.html", "/api/user/login/v1", // "/api/user/add/v1", // "/api/user/speed/test/v1", // "/api/user/say/hello/v1", // "/api/user/*/detail/v1", "/api/crazymaker/duty/info/user/login") .permitAll() .anyRequest().authenticated() // .antMatchers("/image/**").permitAll() // .antMatchers("/admin/**").hasAnyRole("ADMIN") .and() .formLogin().disable() .sessionManagement().disable() .cors() .and() .addFilterAfter(new OptionsRequestFilter(), CorsFilter.class) .apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler()) .and() .apply(new JwtAuthConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout") .and() .logout() // .logoutUrl("/logout") //默认就是"/logout" .addLogoutHandler(tokenClearLogoutHandler()) .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) .and() .addFilterBefore(springSessionRepositoryFilter(), SessionManagementFilter.class) .sessionManagement().disable() ; } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/api/user/login/v1", "/v2/api-docs", "/swagger-resources/configuration/ui", "/swagger-resources", "/swagger-resources/configuration/security", // "/api/user/say/hello/v1", // "/api/user/add/v1", // "/api/user/speed/test/v1", // "/api/user/*/detail/v1", "/images/**", "/swagger-ui.html", "/webjars/**", "**/favicon.ico", "/css/**", "/js/**", "/api/crazymaker/info/user/login" ); } @Resource RedisOperationsSessionRepository sessionRepository; @Resource public CustomedSessionIdResolver httpSessionIdResolver; @DependsOn({"sessionRepository", "httpSessionIdResolver"}) @Bean("jwtAuthenticationProvider") protected AuthenticationProvider jwtAuthenticationProvider() { return new JwtAuthenticationProvider(sessionRepository, httpSessionIdResolver); } public <S extends Session> OncePerRequestFilter springSessionRepositoryFilter() { CustomedSessionRepositoryFilter<? extends Session> sessionRepositoryFilter = new CustomedSessionRepositoryFilter<>( sessionRepository); // sessionRepositoryFilter.setServletContext(this.servletContext); sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver); return sessionRepositoryFilter; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuthenticationProvider()) .authenticationProvider(jwtAuthenticationProvider()); } @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean("daoAuthenticationProvider") protected AuthenticationProvider daoAuthenticationProvider() throws Exception { //这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致 DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider(); daoProvider.setUserDetailsService(userDetailsService()); return daoProvider; } @Bean protected JwtRefreshSuccessHandler jwtRefreshSuccessHandler() { return new JwtRefreshSuccessHandler(); } @Override protected UserDetailsService userDetailsService() { return new UserAuthService(); } @Bean protected JsonLoginSuccessHandler jsonLoginSuccessHandler() { return new JsonLoginSuccessHandler(userAuthService); } @Bean protected TokenClearLogoutHandler tokenClearLogoutHandler() { return new TokenClearLogoutHandler(userAuthService); } @Bean protected CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "HEAD", "OPTION")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.addExposedHeader(SessionConstants.AUTHORIZATION); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
大概通过以上6步,一个集成jwt的springsecurity机制,完整的配置起来了。
具体,请关注 Java 高并发研习社群 【博客园 总入口 】
最后,介绍一下疯狂创客圈:疯狂创客圈,一个Java 高并发研习社群 【博客园 总入口 】
疯狂创客圈,倾力推出:面试必备 + 面试必备 + 面试必备 的基础原理+实战 书籍 《Netty Zookeeper Redis 高并发实战》
Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战
疯狂创客圈 【 博客园 总入口 】
Java 面试题 一网打尽**