赞
踩
自用复习:
资料来源:B站云尚办公-SpringSecurity入门
博客内容:这个文章介绍了springSecurity对于表单提交的 用户名和密码进行认证。
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
正如你可能知道的关于安全方面的两个核心功能是“认证”和“授权”,一般来说,Web 应用的安全性包括**用户认证(Authentication)和用户授权(Authorization)**两个部分,这两点也是 SpringSecurity 重要核心功能。
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。
通俗点说就是系统认为用户是否能登录
(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
通俗点讲就是系统判断用户是否有权限去做某些事情。
要对Web资源进行保护,最好的办法莫过于Filter
要想对方法调用进行保护,最好的办法莫过于AOP
为了防止不合法的用户调用Web资源因此采用了SpringSecurity ,
Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。
如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。
.
.
.
其中过滤器作用:所有过滤器简单解释(博客)
下面这主要看这些过滤器
这里面我们只需要重点关注两个过滤器即可:1.UsernamePasswordAuthenticationFilter
:负责登录认证,这个过滤器是表单认证检验用户名和密码。就是本文要讲的。
2.FilterSecurityInterceptor
:负责权限授权。与本文无关。
说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。
。
。
。
Authentication
(被封装了用户名和密码对象),它存储了认证信息,代表当前登录用户。我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext
来获取Authentication
,SecurityContext
就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder
进行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder
原理非常简单,就是使用ThreadLocal
来保证一个线程中传递同一个对象!
现在我们已经知道了Spring Security中三个核心组件:
1、Authentication
:存储了认证信息,代表当前登录用户
2、SeucirtyContext
:上下文对象,用来获取Authentication
3、SecurityContextHolder
:上下文管理对象,用来在程序任何地方获取SecurityContext
Authentication
中是什么信息呢:
1、Principal
:用户信息,没有认证时一般是用户名,认证后一般是用户对象
2、Credentials
:用户凭证,一般是密码
3、Authorities
:用户权限
Spring Security是怎么进行用户认证的呢?
AuthenticationManager
就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate
方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter
这个过滤器中进行认证的,该过滤器负责认证逻辑。
①对传送过来的表单信息也就是用户名密码进行封装,封装在一个对象中–>authentication
②对authentication对象中的信息进行认证,与数据库中内容进行比较。
…内容一致成功会传入successfulAuthentication方法中,不一致则传入unSuccessfulAuthentication方法。成功后再将其加入springSecurityContext中.
。。。。。
下面请看图:这是上面的流程细节
两个流程,一是封装对象,二是认证。
- 对于认证,首先使用一个authehticate方法进行认证,
this.getAuthenticationManager().authenticate(authRequest)。这个方法由provideManager类实现,然后在authenticate方法中又调用了anthenticate方法(就是委托认证),这个方法来自Authenticationprovider接口由
AbstractUserDetailsAuthenticationProvider类实现。
这个是关于认证的过程,是图1-4的内容。- 在认证过程中如何认证,就是对authentication对象与数据库的对象进行用户名和密码的校验。
校验:
①得到数据库对象, 在委托认证的方法中有一个retrieveUser抽象方法,由DaoAuthenticationProvider类实现,方法中利用UserDetailService通过用户名得到对象,UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username)。
这个是图5-6的内容
②得到数据对象后进行密码校验,调用additionalAuthenticationChecks抽象方法,由DaoAuthenticationProvider类实现,这个方法中利用passwordEncoder组件进行密码校验。
这是图7的内容
this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())
对流程进行小总结:
根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。
这个逻辑没啥好说的,再简单不过了。重点是这里每一个步骤Spring Security都提供了组件:
…
1、是谁执行 根据用户名查询出用户对象 逻辑的呢?用户对象数据可以存在内存中、文件中、数据库中,你得确定好怎么查才行。这一部分就是交由**UserDetialsService
** 处理,该接口只有一个方法loadUserByUsername(String username)
,通过用户名查询用户对象,默认实现是在内存中查询。
…
2、那查询出来的 用户对象 又是什么呢?每个系统中的用户对象数据都不尽相同,咱们需要确认我们的用户数据是啥样的才行。Spring Security中的用户数据则是由**UserDetails
** 来体现,该接口中提供了账号、密码等通用属性。
…
3、对密码进行校验大家可能会觉得比较简单,if、else
搞定,就没必要用什么组件了吧?但框架毕竟是框架考虑的比较周全,除了if、else
外还解决了密码加密的问题,这个组件就是**PasswordEncoder
**,负责密码加密与校验。
UsernamePasswordAuthenticationFilte源码,其中实现这些功能靠的就是attemptAuthentication这个方法。
所以在用户认证这一过程中实际需要我们实现的是:①UserDetialsService这一Service的loadUserByUsername这个方法
②提供一个对象类接收数据库查找的对象UserDetails
③密码校验PasswordEncoder组件。
上代码
1. loadUserByUsername方法
package com.wu.system.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.wu.auth.service.SysMenuService; import com.wu.auth.service.SysUserService; import com.wu.model.system.SysUser; import com.wu.security.custom.CustomUser; //import com.wu.security.interf.UserDetails; import com.wu.system.service.UserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * @Classname UserDetailServiceImpl * @Description * @Date 2023/5/15 21:26 * @Created by cc */ @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired SysUserService sysUserService; @Autowired SysMenuService sysMenuService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, username)); if(user==null) throw new UsernameNotFoundException( "用户名不存在"); if(user.getStatus().intValue()==0) throw new RuntimeException("该用户已停用"); List<String> userPermsList = sysMenuService.findUserPermsList(user.getId()); ArrayList<SimpleGrantedAuthority> authorityArrayList = new ArrayList<>(); for (String userPerms:userPermsList) { authorityArrayList.add(new SimpleGrantedAuthority(userPerms.trim())); } return new CustomUser(user,authorityArrayList); } }
2. UserDetails对象
实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的
org.springframework.security.core.userdetails.User
类,该类实现了UserDetails
接口帮我们省去了重写方法的工作:
添加CustomUser对象
package com.atguigu.security.custom; import com.atguigu.model.system.SysUser; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import java.util.Collection; public class CustomUser extends User { /** * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了) */ private SysUser sysUser; public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) { super(sysUser.getUsername(), sysUser.getPassword(), authorities); this.sysUser = sysUser; } public SysUser getSysUser() { return sysUser; } public void setSysUser(SysUser sysUser) { this.sysUser = sysUser; } }
3.密码组件PasswordEncoder
加密我们项目采取MD5加密
md5加密(需要加依赖):
package com.wu.md5; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public final class MD5 { public static String encrypt(String strSrc) { try { char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; byte[] bytes = strSrc.getBytes(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(bytes); bytes = md.digest(); int j = bytes.length; char[] chars = new char[j * 2]; int k = 0; for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; chars[k++] = hexChars[b >>> 4 & 0xf]; chars[k++] = hexChars[b & 0xf]; } return new String(chars); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("MD5加密出错!!+" + e); } } public static void main(String[] args) { System.out.println(MD5.encrypt("111111")); } }
PasswordEncoder组件:
package com.wu.security.custom; import com.wu.md5.MD5; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * @Classname CustomMd5PasswordEncoder * @Description * @Date 2023/5/15 21:17 * @Created by cc */ @Component public class CustomMd5PasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); } }
当接收请求到认证,遇到了 UsernamePasswordAuthenticationFilter 过滤器,在过滤器的attenmptAuthentication方法中开始了整个用户认证过程。
其源代码:
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username.trim() : ""; String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
自己实现:
package com.wu.security.filter; import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.databind.ObjectMapper; import com.wu.jwt.JwtHelper; import com.wu.result.Result; import com.wu.result.ResultCodeEnum; import com.wu.security.custom.CustomUser; import com.wu.uti.ResponseUtil; import com.wu.vo.system.LoginVo; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 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.HashMap; /** * @Classname TokenLoginFilter * @Description * @Date 2023/5/18 13:27 * @Created by cc */ /** * 通过继承的方式单独实现需要的方法,不是springboot的主流实现方法,可以用@Bean组件尝试 * 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验 */ public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private RedisTemplate redisTemplate; public TokenLoginFilter(AuthenticationManager authenticationManager,RedisTemplate redisTemplate){ this.setAuthenticationManager(authenticationManager); this.setPostOnly(false); //设置请求Authentication对象的路径匹配,就是规定登录路径接口和提交方式 this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST")); this.redisTemplate=redisTemplate; } //登录认证 @SneakyThrows @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword()); //较源码来说少了: this.setDetails(request, authRequest); authRequest=authenticationToken //这句话什么意思 return this.getAuthenticationManager().authenticate(authenticationToken); } //登录成功 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { CustomUser customUser = (CustomUser) authResult.getPrincipal(); String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername()); logger.info("customer中的authority:"+customUser.getAuthorities().toString()); //保存数据权限 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.string()); redisTemplate.opsForValue().set(customUser.getUsername().trim(), JSON.toJSONString(customUser.getAuthorities())); logger.info("redis存的数据:"+ redisTemplate.opsForValue().get(customUser.getUsername())); HashMap<String, Object> map = new HashMap<>(); map.put("token", token); ResponseUtil.out(response, Result.ok(map)); } // 登录失败 @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { super.unsuccessfulAuthentication(request, response, failed); if(failed.getCause() instanceof RuntimeException) { ResponseUtil.out(response, Result.build(null, 204, failed.getMessage())); } else { ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBEL_ERROR)); } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。