Spring Security
- 前言
- Spring Security
- 入门编辑
- Spring Security底层原理
- UserDetailsService接口
- PasswordEncoder接口
- 认证
- 登录
- 校验
- 密码加密存储
- 退出登录
前言
本文是作者学习三更老师的Spring Security课程所记录的学习心得和笔记知识,希望能帮助到大家
Spring Security
Spring Security基于Spring框架,提供了‘一套Web应用安全性的完整解决方案
Web应用安全性包括用户认证和用户授权是SpringSecurity的核心功能
- 用户认证
系统认为用户是否能登录
- 用户授权
判断用户是否有权限去做某些事情
入门编辑
第一步:搭建SpringBoot环境
第二步:导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
编写一个测试类创建项目
@RestController
public class logincontroller {
@GetMapping("/hello")
public String vos(){
return "Hello,security";
}
}
启动项目后会发现需要进行登录认证
默认用户名:user
密码:
Spring Security底层原理
SpringSecurity的底层就是一个过滤器链
FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤链的最底层
ExceptionTranslationFilter:是一个异常过滤器,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter:对/login的POST请求做拦截,校验表单中的用户名,密码
UserDetailsService接口
UserDetailsService接口:当什么也没有配置的时候,账号和密码是由Spring Security定义生成的,而在实际项目中账号和密码都是从数据库中查询出来的,所以要通过自定义逻辑控制认证逻辑
PasswordEncoder接口
数据加密接口,用于返回User对象里面密码加密
通过BCryptPasswordEncorder 对象对密码进行加密
认证
认证流程:
在前端发送携带用户登录信息的请求的时候,会到达UsernamePasswordAuthenticationFilter过滤器当中,过滤器将用户信息封装成一个Authentication对象,其中只存在用户名和密码,然后通过调用方法aythenticate一步一步向下认证,最后通过用户名在内存中进行查找,把对应的信息封装到UserDetils对象当中去,返回的时候通过PasswordEncoder对比密码,正确返回,这时默认的用户登录的流程。
我们一般采用的是在数据库中进行查询对应的用户信息,如果查询正确生成JWT信息返回给前端界面,此处我们可以自己创建一个controller类代替UsernamePasswordAuthenticationFilter过滤器,在最后可以自定义UserDetailsService的实现类完成在数据库中的查询
这是生成一个JWT的过程,那么如何去校验JWT信息:
创建一个jwt认证过滤器(获取token,解析token,获取userid,封装Authentication对象存入SecurityContextHolder)
整体的认证思路:
登录:
一:自定义登录接口
调用ProviderManager的方法进行认证,如果认证通过生成jwt
把用户信息存入到redis中
二:自定义UserDetilsService
在这个实现中去查询数据库
校验:
一:定义jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入到securityContextHolder
登录
自定义UserDetilsService方法:
@Service
public class userdetilservice implements UserDetailsService {
@Autowired
private Usermapper usermapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> lambdaQueryWrapper=new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUsername,username);
org.apache.catalina.User user = usermapper.selectOne(lambdaQueryWrapper);
if(Objects.isNull(user))
{
throw new RuntimeException("用户名或者密码错误");
}
return null;
}
}
这里是要将信息封装到UserDetils中,我们需要创建一个实体类继承UserDetils
@Data
@NoArgsConstructor
@AllArgsConstructor
public class userdetilsimpl implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getAge();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
运行测试,在进行运行的时候会报出,passwordEncoder的配对的值为null,这时我们需要在数据库的密码数据中加入前缀{noop},这样表示该密码进行明文保存
- 自定义登录接口
将AuthenticationManager注入到容器当中
在上述所讲的配置类中继承的WebsecurityConfigurerAdapter中可以重写方法完成AuthenticationManager的配置进行用户认证
@Bean
public AuthenticationManager authenticationManager() throws Exception{
return super.authenticationManagerBean();
}
通过三层架构完善登录功能:在service的实现层impl中注入AuthenticationManager
封装之后可以通过它所提供的方法:authenticate进行认证,参数需要一个authentication对象,由于它是一个接口但是我们的框架中提供了它的实现对象,通过登录的用户名和密码可以创建一个authentication对象
@Service
public class loginserviceimpl implements loginservice {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void login(User user) {
//进行用户认证
//在登录之后将用户名和密码封装成一个Authentiaion对象
//
UsernamePasswordAuthenticationToken us=new UsernamePasswordAuthenticationToken(user.getUsername(),user.getAge());
Authentication authenticate = authenticationManager.authenticate(us);
//判断是否认证通过
if(Objects.isNull(authenticate)){
throw new RuntimeException("登陆失败");
}
//如果通过了使用userid生成一个jwt,jwt
User user1 = (User) authenticate.getPrincipal();
String string = user1.getId().toString();
String jwt=JwtUtil.createJWT(string);
Map<String,String> map=new HashMap<>();
map.put("token",jwt);
return new ResponseResult(200,"登录成功",map);
}
}
校验
创建一个过滤器,之前的javaWeb讲解中过滤器都实现Filter,在这里我们继承OncePerRequestFilter就可,这样会使得前端请求来的请求只经过该过滤器一次
过滤器代码:
@Component
public class AuthonJwtfilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = httpServletRequest.getHeader("token");
//判断token是否为空
if(!StringUtils.hasText(token))
{
//放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 解析token
try {
Claims claims=JwtUtil.parseJWT(token);
String id=claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String rediskey="login:"+id;
User user=redisCache.getCacheObject(rediskey);
if(!Objects.isNull(user))
{
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(user,null,null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
完成后需要在配置类中将jwt过滤器放在过滤链中:
在配置类的配置方法configure中配置
http.addFilterBefore(jwtAuthenticationTokenFilter,usernamePasswordAunthencationFilter.class);
密码加密存储
实际项目中我们不会将密码明文保存在数据库中,默认的PasswordEncoder要求数据库中的密码格式为:{id}password根据id进行判断加密方式
我们一般所用的是SpringSAecurity为我们提供的BCryptPasswordEncorder
我们将BCryptPasswordEncorder注入容器中,我们就可以根据passwordEncoder进行密码判断
我们需要进行Spring Security进行配置类:
@Configuration
public class securityconfig extends WebSecurityConfigurerAdapter {
@Bean
//创建BCryptPasswordEncoder注入容器
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
我们可以使用encode方法将明文密码进行加密
可以使用matches方法将密码与加密后的方法判断是否匹配
@Test
public void TestBC(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//对数据进行加密
//使用盐和加密
String encode = bCryptPasswordEncoder.encode("123456");
bCryptPasswordEncoder.matches("1234","加密之后的密文");
}
}
退出登录
如何退出登录:
我们只需要定义一个登录接口,然后获取securityContextHolder中的认证信息,删除redis中的数据,这样就会退出登录
也是通过三层架构进行退出登录操作,在服务实现类中:
public class zhuxiaoimpl {
public ResponseResult logout(){
UsernamePasswordAuthenticationToken authenticationToken= (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
User user= (User) authenticationToken.getPrincipal();
String string = user.getId().toString();
//删除redis中的值
redisCache.delete(string);
return new ResponseResult(200,"注销成功");
}
}
}
注意,在其中还应该删除SecurityContextHolder中保存的值,要不然就算登出,还是会保持认证状态的