登录引入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:处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException

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进行认证

因为ProviderManagerAuthenticationManager的实现类,需要注入AuthenticationManager,然后去调用AuthenticationManagerauthenticate方法,这个方法需要一个参数,也就是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方法进行认证后,会去调用UserDetailsServiceloadUserByUsername方法查询用户,因为它默认调用的是内存中的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对象,然后就需要用它让UserDetailsAuthentication对象中的密码进行比对,如果正确把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则成功接收到数据

posted @ 2022-08-08 13:26  长情c  阅读(47)  评论(0)    收藏  举报