spring security(1)https://developer.aliyun.com/article/1531008
Redis工具类
package com.sucurity.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; @SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component //redis工具类 public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 删除Hash中的数据 * * @param key * @param hkey */ public void delCacheMapValue(final String key, final String hkey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hkey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }
webUtils
package com.sucurity.utils; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class WebUtils { /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
User类
二,认证
2.2 原理
2.2.1 完整流程
SpringSecurity就是一个过滤器链,内部提供各种功能的过滤器
UsernamePasswordAuthentionFilter: 负责处理登陆页面填写账号密码后登录的请求
ExceptionTranslationFilter: 负责处理过滤器链中的抛出的AccessDeniedException
和AuthenticationException
FilterSecurityInterceptor: 负责处理权限校验
debug产看过滤器和他们的顺序
2.2.2 认证流程详解
一、Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息
二、AuthenticationManager接口:定义了认证Authentication的方法
三、UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法
四、UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中
2.3. 自定义security的思路
2.3.1 思路分析
登录
- 自定义登录接口
- 调用ProviderManager的方法进行认证,如果认证通过生成jwt
- 把用户信息放入UserDetailService
- 自定义UserDetailService
- 查询数据库
校验
- 定义jwt认证过滤器
- 获取token
- 解析token
- 从redis 中获取用户信息
- 存入SecurityContexHolder
3.自定义实现
也是需要查询用户信息然后封装成UserDetails对象返回
3.1 重写 UserDetailsService
3.2 重写UserDetails
package com.sucurity.domain; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; public class LoginUser implements UserDetails { private User user; public void setUser(User user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return null; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
3.3. 密码加密校验问题
上面我们实现了自定义Security的认证机制,让Security根据数据库的数据,来认证用户输入的数据是否正确。但是当时存在一个问题,就是我们在数据库存入用户表的时候,插入的huanf用户的密码是 {noop}112233,为什么用112233不行呢
原因: SpringSecurity默认使用的PasswordEncoder要求数据库中的密码格式为:{加密方式}密码。对应的就是{noop}112233,实际表示的是112233
但是我们在数据库直接暴露112233为密码,会造成安全问题,所以我们需要把加密后的1234的密文当作密码,此时用户在浏览器登录时输入1234,我们如何确保用户能够登录进去呢,答案是SpringSecurity默认的密码校验,替换为SpringSecurity为我们提供的BCryptPasswordEncoder
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。我们可以定义一个SpringSec
package com.sucurity.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
密码1234 对应$2a$10$zOitKu6UNk.b/iPFTtIj2u80sH/dfJI9vFr57qhDGteuXj/Wl8uSy
3.4 登录接口
在接口中通过AuthenticationManager 的authenticate 方法来进行用户认证,所以需要在SecurityConfig 中配置把AuthenticationManage注入到容器
认证成功生成一个jwt 放入响应中返回,并把相应的用户信息存入redis
SecurityConfig配置
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http //由于是前后端分离项目,所以要关闭csrf .csrf().disable() //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //指定让spring security放行登录接口的规则 .authorizeRequests() // 对于登录接口 anonymous表示允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); }
3.5 认证过滤器
在上面学习的 ‘认证’ 的 ‘3. 自定义security的思路’ 当中,我们有一个功能需求是定义Jwt认证过滤器,这个功能还没有实现,下面就正式学习如何实现这个功能。要实现Jwt认证过滤器,我们需要获取token,然后解析token获取其中的userid,还需要从redis中获取用户信息,然后存入SecurityContextHolder
默认的是User那个过滤器把request中的用户信息存入SecurityContextHolder
我们自定义登录逻辑后request中肯定就没有了原来的登录参数,User这个过滤器就不会放入用户信息到SecurityContextHolder
为什么要有redis参与: 是为了防止过了很久之后,浏览器没有关闭,拿着token也能访问,这样不安全
认证过滤器的作用是什么: 上面我们实现登录接口的时,当某个用户登录之后,该用户就会有一个token值,我们可以通过认证过滤器,由于有token值,并且token值认证通过,也就是证明是这个用户的token值,那么该用户访问我们的业务接口时,就不会被Security拦截。简单理解作用就是登录过的用户可以访问我们的业务接口,拿到对应的资源
第一步: 定义过滤器。在 src/main/java/com.huanf 目录新建 filter.JwtAuthenticationTokenFilter 类,写入如
package com.sucurity.filter; import com.huanf.domain.LoginUser; import com.huanf.utils.JwtUtil; import com.huanf.utils.RedisCache; import com.sucurity.domain.LoginUser; import com.sucurity.utils.JwtUtil; import com.sucurity.utils.RedisCache; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; /** * @author 35238 * @date 2023/7/12 0012 14:07 */@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token,指定你要获取的请求头叫什么 String xxtoken = request.getHeader("token"); //判空,不一定所有的请求都有请求头,所以上面那行的xxtoken可能为空 //!StringUtils.hasText()方法用于检查给定的字符串是否为空或仅包含空格字符 if (!StringUtils.hasText(xxtoken)) { //如果请求没有携带token,那么就不需要解析token,不需要获取用户信息,直接放行就可以 filterChain.doFilter(request, response); //return之后,就不会走下面那些代码 return; } //解析token String userid; //把userid定义在外面,才能同时用于下面的46行和52行 try { Claims claims = JwtUtil.parseJWT(xxtoken); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //从redis中获取用户信息 String redisKey = "login:" + userid; //LoginUser是我们在domain目录写的实体类 LoginUser loginUser = redisCache.getCacheObject(redisKey); //判断获取到的用户信息是否为空,因为redis里面可能并不存在这个用户信息,例如缓存过期了 if(Objects.isNull(loginUser)){ //抛出一个异常 throw new RuntimeException("用户未登录"); } //把最终的LoginUser用户信息,通过setAuthentication方法,存入SecurityContextHolder //TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = //第一个参数是LoginUser用户信息,第二个参数是凭证(null),第三个参数是权限信息(null) new UsernamePasswordAuthenticationToken(loginUser,null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //全部做完之后,就放行 filterChain.doFilter(request, response); } }
Spring Security中permitAll()和anonymous()的区别
anonymous() 允许匿名用户访问,不允许已登入用户访问
permitAll() 不管登入,不登入 都能访问
permitAll(): Always evaluates to true
isAnonymous(): Returns true if the current principal is an anonymous user
从 Spring文档:
采用“默认拒绝”通常被认为是良好的安全实践,您可以明确指定允许的内容并禁止其他所有内容。定义未经身份验证的用户可以访问的内容是类似的情况,尤其是对于 Web 应用程序。许多站点要求用户必须通过除少数 URL 之外的任何其他内容(例如主页和登录页面)的身份验证。在这种情况下,最容易为这些特定 URL 定义访问配置属性,而不是为每个受保护的资源定义访问配置属性。换句话说,有时可以说默认情况下需要 ROLE_SOMETHING 并且只允许此规则的某些例外情况,例如登录、注销和应用程序的主页。您也可以完全从过滤器链中省略这些页面,从而绕过访问控制检查,
这就是我们所说的匿名身份验证。
请注意,“经过匿名身份验证”的用户和未经身份验证的用户之间没有真正的概念差异。Spring Security 的匿名身份验证只是为您提供了一种更方便的方式来配置您的访问控制属性。
使用.permitAll()will 配置授权,以便在该特定路径上允许所有请求(来自匿名用户和登录用户)。
的.anonymous()表达主要是指用户(登录与否)的状态。基本上,在用户通过“身份验证”之前,它是“匿名用户”。这就像每个人都有一个“默认角色”。
修改SecurityConfig 修改过滤器的执行顺序
@Autowired //注入我们在filter目录写好的类 private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; //---------------------------登录接口的实现---------------------------------- @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http //由于是前后端分离项目,所以要关闭csrf .csrf().disable() //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //指定让spring security放行登录接口的规则 .authorizeRequests() // 对于登录接口 anonymous表示允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //---------------------------认证过滤器的实现---------------------------------- //把token校验过滤器添加到过滤器链中 //第一个参数是上面注入的我们在filter目录写好的类,第二个参数表示你想添加到哪个过滤器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); }
这样之后拿着token就可以访问除了登录之外的接口
spring security(3)https://developer.aliyun.com/article/1531014