登录引入security解析
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
登录校验流程
security完整流程
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:
负责权限校验的过滤器。
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:
处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。
FilterSecurityInterceptor:
负责权限校验的过滤器。
认证流程详解
概念速查:
Authentication
接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager
接口:定义了认证Authentication
的方法
UserDetailsService
接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails
接口:提供核心用户信息。通过UserDetailsService
根据用户名获取处理的用户信息要封装成UserDetails
对象返回。然后将这些信息封装到Authentication对象中。
案例
现在有一个登录功能,我们需要引入security那么怎么实现呢?
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现类中去查询数据库
注意配置passwordEncoder为BCryptPasswordEncoder
1.自定义登录接口
@RestController
public class BlogLoginController {
@Autowired
private BlogLoginService blogLoginService;
@PostMapping("/login")
public ResponseResult login(@RequestBody User user){
return blogLoginService.login(user);
}
}
2.调用ProviderManager进行认证
因为ProviderManager
是AuthenticationManager
的实现类,需要注入AuthenticationManager
,然后去调用AuthenticationManager
的authenticate
方法,这个方法需要一个参数,也就是Authentication
对象,但是它也是一个接口,那么就可以使用它的实现类UsernamePasswordAuthenticationToken
来创建对象
package com.mrs.service.impl;
import com.mrs.common.ResponseResult;
import com.mrs.common.vo.BlogUserLoginVo;
import com.mrs.common.vo.UserInfoVo;
import com.mrs.entity.LoginUser;
import com.mrs.entity.User;
import com.mrs.service.BlogLoginService;
import com.mrs.utils.BeanCopyUtils;
import com.mrs.utils.JwtUtil;
import com.mrs.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* description: BlogLoginServiceImpl
* date: 2022/8/7 22:06
* author: MR.孙
*/
@Service
public class BlogLoginServiceImpl implements BlogLoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//因为AuthenticationManager会通过UserDetailsService去比对用户密码,UserDetailsService比对完后会
//AuthenticationManager.authenticate会有一个返回值,里面封装了UserDetails等信息
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
return null;
}
}
要注入AuthenticationManager
,但是没有配置Security,所以我们需要去配置一下
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
3.调用UserDetailsService进行比对
Authentication
调用authenticate
方法进行认证后,会去调用UserDetailsService
的loadUserByUsername
方法查询用户,因为它默认调用的是内存中的UserDetailsService
这显然是不行的,我们需要的是在数据库中查询用户,所以我们需要去创建一个UserDetailsService
的实现类。
package com.mrs.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mrs.entity.LoginUser;
import com.mrs.entity.User;
import com.mrs.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* description: UserDetailsServiceImpl
* date: 2022/8/7 22:24
* author: MR.孙
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查看用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//判断是否查到用户,如果没有查到用户就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
//返回用户信息
//TODO 查询权限信息封装
return new LoginUser(user);
}
}
**这里的返回值需要注意一下,因为返回的是个UserDetails
接口,但是我们不能返回UserDetails
接口,所以我们需要一个UserDetails
接口,那么怎么实现呢?第一种就是在User对象里面实现,这样杂烩在一起并不好,第二种就是单独创建一个实体类实现UserDetails
接口
**
package com.mrs.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* description: LoginUser
* date: 2022/8/7 22:37
* author: MR.孙
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@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进行比对
package com.mrs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* description: SecurityConfig
* date: 2022/8/7 22:11
* author: MR.孙
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//使用BCryptPasswordEncoder进行加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
http.logout().disable();
//允许跨域
http.cors();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
UserDetailsService
实现类编写完成后它会返回一个UserDetails
对象,然后就需要用它让UserDetails
和Authentication
对象中的密码进行比对,如果正确把UserDetails
中的权限信息设置到Authentication
对象中,也就是继续返回到登录的service实现类中
然后在登录的实现类中需要判断Authentication
,如果通过
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//因为AuthenticationManager会通过UserDetailsService去比对用户密码,UserDetailsService比对完后会
//AuthenticationManager.authenticate会有一个返回值,里面封装了UserDetails等信息
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//判断是否通过
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或者密码错误");
}
//获取userId,生成token
LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//把用户信息存入到redis中
redisCache.setCacheObject("bloglogin:"+userId,loginUser);
//把token和userInfo封装返回
UserInfoVo userInfo = BeanCopyUtils.copyBean(loginUser.getUser(),UserInfoVo.class);
BlogUserLoginVo blogUserLoginVo = new BlogUserLoginVo(jwt, userInfo);
return ResponseResult.okResult(blogUserLoginVo);
}
登录校验过滤器
思路
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
JwtAuthenticationTokenFilter
为什么需要这个登录认证过滤器呢?当我们后端生成完token后,前端是会将它存储到请求头中的,这样每次请求都会携带token,这里这个登录认证过滤器的作用就是解析这个token,并将用户信息存储到SecurityContextHolder中,方便其他类调用获取这个用户信息。
这个认证过滤器执行一次过滤器链即可OncePerRequestFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){
//如果请求头中没有token,说明该接口不需要登录,直接放行,让可以执行后面的过滤器
filterChain.doFilter(request,response);
return ;
}
//从token中解析获取userid
Claims claims =null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//出现异常说明token超时或者token被篡改(非法)
//抛出异常信息,响应告诉前端需要重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return ;
}
//从redis中获取用户信息
String userId = claims.getSubject();
LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
//判断redis中是否有用户信息
if(Objects.isNull(loginUser)){
//没有获取到说明登录过期,提示重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return ;
}
//存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
这里需要注意的是UsernamePasswordAuthenticationToken
这个对象的参数,两个参数代表没有认证,三个参数代表已认证,实际上执行到这一步其实已经是认证过了,所以是三个参数
SecurityConfig
package com.mrs.config;
import com.mrs.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* description: SecurityConfig
* date: 2022/8/7 22:11
* author: MR.孙
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//使用BCryptPasswordEncoder进行加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/link/getAllLink").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
//把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.logout().disable();
//允许跨域
http.cors();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
这里为了测试我们让一个接口必须认证过才可以访问
并且配置过滤器链
当登录后,服务器返回一个token,然后去访问那个必须认证的接口才能访问的接口(也就是security配置中的/link/getAllLink接口)
如果没有token则是提示没有登录
当携带token则成功接收到数据