一、介绍
引用SpringSecurity 官网的一句话:
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring 安全性是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
Spring 安全性是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring 安全性的真正力量在于它可以轻松扩展以满足自定义需求。
SpringSecurity是Spring体系下的一个安全框架,常用于用户认证与角色授权相关;因为出自于Spirng家族,所以与Spring高度融合。使用的话个人感觉较Shiro更难一些,但是如果了解其中一个的话,对于学习另一个来说也是非常快的。
其中的原理的话这里就不详细说了,直接以SpringBoot集成SpringSecurity实现前后端分离下的用户认证与授权,这里是用SpringSecurity最新的配置,相较于以前的版本有一些变化,不过大差不差。。。
二、引入依赖 <parent > <artifactId > spring-boot-starter-parent</artifactId > <groupId > org.springframework.boot</groupId > <version > 2.6.9</version > </parent > <dependency > <artifactId > spring-boot-starter-web</artifactId > <groupId > org.springframework.boot</groupId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
三、代码编写 AuthenticationEntryPointImpl(认证异常处理) @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ServletUtil.renderString(response, JSON.toJSONString(R.fail("访问异常,请登录" , 401 ))); } } public static void renderString (HttpServletResponse response, String string) { PrintWriter writer = null ; try { response.setStatus(200 ); response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); writer = response.getWriter(); writer.print(string); } catch (IOException e) { e.printStackTrace(); } finally { writer.flush(); writer.close(); } }
AccessDeniedHandlerImpl(授权异常处理) @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ServletUtil.renderString(response, JSON.toJSONString(R.fail("访问异常,无访问权限" , 403 ))); } }
LoginUser(用于存储用户登录信息) 这个类的话实现SpringSecurity的UserDetails接口,SpringSecurity上下文访问用户登录信息依靠此类,此类高度自定义,可以根据自己需要的功能自定义()
public class LoginUser implements UserDetails { private Long userId; private User user; private Long loginTime; private Long expireTime; public LoginUser (User user) { this .userId = user.getUserId(); this .user = user; } @Override public Collection<? extends GrantedAuthority > getAuthorities() { return Arrays.asList(new SimpleGrantedAuthority ( user.getUserType().toString())); } @Override public String getPassword () { return user.getPassword(); } @Override public String getUsername () { return user.getUsername(); } @JSONField(serialize = false) @Override public boolean isAccountNonExpired () { return true ; } @JSONField(serialize = false) @Override public boolean isAccountNonLocked () { return true ; } @JSONField(serialize = false) @Override public boolean isCredentialsNonExpired () { return true ; } @JSONField(serialize = false) @Override public boolean isEnabled () { return true ; } public Long getUserId () { return userId; } public void setUserId (Long userId) { this .userId = userId; } public User getUser () { return user; } public void setUser (User user) { this .user = user; } public Long getLoginTime () { return loginTime; } public void setLoginTime (Long loginTime) { this .loginTime = loginTime; } public Long getExpireTime () { return expireTime; } public void setExpireTime (Long expireTime) { this .expireTime = expireTime; } }
AuthenticationContextHolder(定义存储Authentication的ThreadLocal) 为什么要使用ThreadLocal,因为以下UserDetailsServiceImpl中的loadUserByUsername我们不能主动调用,要通过SpringSecurity来调用,其中的入参username就是从Authentication中获取。对于ThreadLocal来说是一个容器,在多线程下保存每一个线程的副本,因为对于登录来说,如果同一时间多个人登录,可能照成获取到username产生混乱,ThreadLocal的多线程安全则避开了这一情况。如果说有更好的方法,也可自定义。
public class AuthenticationContextHolder { private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal <>(); public static Authentication getContext () { return contextHolder.get(); } public static void setContext (Authentication context) { contextHolder.set(context); } public static void clearContext () { contextHolder.remove(); } }
UserDetailsServiceImpl(此类用于查询用户的信息,登录时供SpringSecurity调用) @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { User user = userMapper.selectOne(new LambdaQueryWrapper <User>() .eq(User::getUsername, username)); if (ObjUtil.isNull(user)) { throw new UserException ("账号不存在" ); } else if (user.getStatus() == 0 ) { throw new UserException ("账号被锁定" ); } Authentication authentication = AuthenticationContextHolder.getContext(); String password = authentication.getCredentials().toString(); boolean pwdPass = SecurityUtil.matchesPassword(password, user.getPassword()); if (!pwdPass) { throw new UserException ("密码错误" ); } authentication.remove(); return new LoginUser (user); } }
AesPasswordEncoder与SecurityUtil,密码加密类与安全工具类 public class AesPasswordEncoder implements PasswordEncoder { public static final String KEY = "wangqingzezzzzzz" ; @Override public String encode (CharSequence rawPassword) { try { byte [] raw = KEY.getBytes("utf-8" ); SecretKeySpec skeySpec = new SecretKeySpec (raw, "AES" ); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding" ); cipher.init(Cipher.ENCRYPT_MODE, skeySpec); byte [] encrypted = cipher.doFinal(rawPassword.toString().getBytes("utf-8" )); return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { e.printStackTrace(); return null ; } } @Override public boolean matches (CharSequence rawPassword, String encodedPassword) { return rawPassword.toString().equals(decode(encodedPassword)); } public String decode (String rawPassword) { try { byte [] raw = KEY.getBytes("utf-8" ); SecretKeySpec skeySpec = new SecretKeySpec (raw, "AES" ); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding" ); cipher.init(Cipher.DECRYPT_MODE, skeySpec); byte [] decode = Base64.getDecoder().decode(rawPassword); byte [] original = cipher.doFinal(decode); String originalString = new String (original, "utf-8" ); return originalString; } catch (Exception e) { e.printStackTrace(); return null ; } } } public class SecurityUtil { public static Long getUserId () { Long userId = getLoginUser().getUserId(); return userId; } public static LoginUser getLoginUser () { try { return (LoginUser) getAuthentication().getPrincipal(); } catch (Exception e) { throw new UserException ("获取用户信息异常" ); } } public static Authentication getAuthentication () { return SecurityContextHolder.getContext().getAuthentication(); } public static String encryptPassword (String password) { AesPasswordEncoder passwordEncoder = new AesPasswordEncoder (); return passwordEncoder.encode(password); } public static boolean matchesPassword (String rawPassword, String encodedPassword) { AesPasswordEncoder passwordEncoder = new AesPasswordEncoder (); return passwordEncoder.matches(rawPassword, encodedPassword); } public static String decodePassword (String rawPassword) { AesPasswordEncoder passwordEncoder = new AesPasswordEncoder (); return passwordEncoder.decode(rawPassword); } }
JwtAuthenticationTokenFilter(登录认证过滤) @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Resource private JwtUtil jwtUtil; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { LoginUser loginUser = jwtUtil.getLoginUser(request); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (ObjUtil.isNotNull(loginUser) && ObjUtil.isNull(authentication)) { jwtUtil.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (loginUser, null , loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource ().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(request, response); } } public void verifyToken (LoginUser loginUser) { long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <= 10 * MINUTE) { refreshToken(loginUser); } } public void refreshToken (LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expire * MINUTE); String userKey = RedisKey.LOGIN_USER_KEY + (loginUser.getUserId()); redisUtil.set(userKey, loginUser, expire); }
LogoutService(退出登录处理器,不需要我们编写退出登录的Service,使用SpringSecurity自带的) @Service public class LogoutService implements LogoutSuccessHandler { @Resource private JwtUtil jwtUtil; @Resource private RedisUtil redisUtil; @Override public void onLogoutSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { LoginUser loginUser = jwtUtil.getLoginUser(request); if (ObjUtil.isNotNull(loginUser)) { redisUtil.del(RedisKey.LOGIN_USER_KEY + loginUser.getUserId()); } ServletUtil.renderString(response, JSON.toJSONString(R.ok("退出登录成功" ))); } }
SpringSecurityConfig(SpringSecurity的核心配置) @Configuration @EnableWebSecurity public class SpringSecurityConfig { @Resource private AuthenticationEntryPointImpl authenticationEntryPoint; @Resource private AccessDeniedHandlerImpl accessDeniedHandler; @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Resource private UserDetailsService userDetailsService; @Resource private LogoutService logoutService; @Bean public AuthenticationManager authenticationManager (AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { return http .exceptionHandling(handle -> handle .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ) .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests(auth -> auth .antMatchers("/favicon.ico" , "/doc.html" , "/webjars/**" , "/swagger-resources/**" , "/v3/api-docs/**" ).permitAll() .antMatchers("/user/book/syncNotify" ,"/user/book/asyncNotify" ).permitAll() .antMatchers("/user/login/**" , "/manage/login/login" ).permitAll() .antMatchers("/user/**" ).hasAuthority("0" ) .antMatchers("/manage/**" ).hasAuthority("1" ) .anyRequest().authenticated() ) .logout().logoutUrl("/*/logout" ).logoutSuccessHandler(logoutService).and() .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) .userDetailsService(userDetailsService) .build(); } @Bean public PasswordEncoder passwordEncoder () { return new AesPasswordEncoder (); } @Bean public CorsConfigurationSource corsConfigurationSource () { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); source.registerCorsConfiguration("/**" , new CorsConfiguration ().applyPermitDefaultValues()); return source; } }
四、关于其中一些细节部分,以及相关的注解配置权限,以后再来更新。。。。。。