SpringSecurity(一)

发布于:2022-12-12 ⋅ 阅读:(459) ⋅ 点赞:(0)

SpringSecurity(一)——基本的登录使用

一、简述SpringSecurity

SpringSecurity是一个能够为基于SPring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它充分利用了Spring的IOC、DI和AOP功能,为系统提供了声明式的安全访问控制功能,减少了为企业系统安全控制开发大量的重复代码。
SpringSecurity对Web安全性的支持大量的依赖于Servlet的Filter。这些过滤器拦截进入系统的请求,并且在处理请求钱进行某些安全的验证。SpringSecurity提供了多个过滤器,这些过滤器拦截了Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。这也使得开发人员可以根据自己的需求,适当的使用过滤器来进行自我的保护。

二、开始

1、导入依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2、启动项目

在这里插入图片描述
在这里插入图片描述
默认的用户为:user

3、入门流程图

在这里插入图片描述

注释:

  1. UsernamePasswordAuthenticationFilter: 是用户名和密码的主要处理类,构造出UsernamePasswordAuthenticationToken类,将用户的信息封装到Authentication中;
  2. Authentication接口:封装了用户的信息;
  3. AuthenticationManager接口:定义了Authentication认证的方法,认证的出发点,也是认证的核心接口。日常登录的方式存在:用户名和密码、手机号和密码、邮箱和密码等多种方式。
  4. DaoAuthenticationProvider::用于解析并认证 UsernamePasswordAuthenticationToken 的这样一个认证服务提供者,对应以上的几种登录方式。
  5. UserDetailsService接口:SpringSecurity会将username传递给后端UserDetailsServiceImpl类的loadbyUsername方法,根据提供的username到数据库中查询出指定的用户信息,并将信息封装到UserDetails对象中返回给SpringSecurity,并且由SpringSecurity完成密码的比对;
  6. UserDetails接口:用户的核心信息。根据username在UserDetailsService的loadByUsername中查询对应的数据封装到UserDetails对象中,最后封装到Authentication对象中。
    7.在这里插入图片描述
    UsernamePasswordAuthenticationFilter: 是我们最常用的用户名和密码认证方式的主要处理类,构造了一个UsernamePasswordAuthenticationToken对象实现类,将用请求信息封装为Authentication;
    BasicAuthenticationFilter…: 将UsernamePasswordAuthenticationFilter的实现类UsernamePasswordAuthenticationToken封装成的 Authentication进行登录逻辑处理;
    ExceptionTranslationFilter: 主要用于处理AuthenticationException(认证)和AccessDeniedException(授权)的异常;
    FilterSecurityInterceptor: 获取当前 request 对应的权限配置**,**调用访问控制器进行鉴权操作;

4、创建登录表

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
  `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  COMMENT='用户表';

5、完成基本的dao、Mapper、测试可以正常使用

  1. 创建User实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;

    /**
     * 主键
     */
    @TableId
    private Long id;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 密码
     */
    private String password;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机号
     */
    private String phonenumber;
    /**
     * 用户性别(0男,1女,2未知)
     */
    private String sex;
    /**
     * 头像
     */
    private String avatar;
}
  1. 因为SpringSecurity需要返回的为UserDetails

@Data
@NoArgsConstructor
@AllArgsConstructor
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();
    }

    /**
     * 是否未过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 是否未锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 凭证是否未过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. 实现UserDetailService接口并且重写loadUserByUsername方法

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 通过重写loadByUsername方法来自定义登录的逻辑
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 到数据库中根据用户名查询用户信息
        User userInfo = userMapper.selectOne(new QueryWrapper<User>().eq("user_name", username));
        //如果没有查询到用户
        if (Objects.isNull(userInfo)){
            throw new RuntimeException("用户名或者密码错误");
        }


        // 将用户信息封装到UserDetails中  返回给SpringSecurity
        return new LoginUser(userInfo);
    }
}

注: 到此基本搭建完成,但是SpringSecurity的密码是存储的加密的,若使用明文密码需要在密码的前面添加前缀“{noop}”。

6、针对密码的处理

SpringSecurity提供了加密的方式,一般采用的是BCryptPasswordEncoder,将BCryptPasswordEncoder注入到Spring中,SpringSecurity就会使用PasswordEcoder进行密码的校验。
可以通过定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

7、准备登录接口

Controller层

@PostMapping("/login")
    public ResponseResult login(@RequestBody User user) {
        ResponseResult login = loginService.login(user);
        return login;
    }

流程:
1、SecurityConfig 继承 WebSecurityConfigurerAdapter 并重写configure()方法,对login接口进行放行,因为用户认证需要使用AuthenticationManager的authenticate方法,因此需要在SecurityConfig配置中将AuthenticationManager注入到容器。


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @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().authenticated();


        //  把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

2、认证过滤器:
需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder


@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 token = request.getHeader("token");
        // 判断token是否为空
        if (StringUtils.hasText(token)) {
            //放行
            //解析token
            String userid;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                userid = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("token非法");
            }
            //从redis中获取用户信息
            String redisKey = "login:" + userid;
            LoginUser loginUser = redisCache.getCacheObject(redisKey);
            if(Objects.isNull(loginUser)){
                throw new RuntimeException("用户未登录");
            }
            //封装Authentication对象存入SecurityContextHolder
            //TODO 获取权限信息封装到Authentication中
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser,null,null);
        	SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        //放行
        filterChain.doFilter(request, response);
    }
}

SecurityConfig中configure中添加过滤

 //  把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

3、Service层

/**
     * 登录  如果认证通过,使用user生成jwt  jwt存入ResponseResult 返回
     * @param user
     * @return
     */
    @Override
    public ResponseResult login(User user) {
        // 通过UsernamePasswordAuthenticationToken获取用户名和密码
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        //AuthenticationManager委托机制对authenticationToken 进行用户认证
        Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

        //如果认证没有通过,给出对应的提示
        if (Objects.isNull(authentication)){
            throw new RuntimeException("登录失败");
        }
        //如果认证通过,拿到这个当前登录用户信息
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        //获取当前用户的userid
        String userid = loginUser.getUser().getId().toString();
        String jwt = JwtConfig.createJWT(userid);
        Map<String, String> map = new HashMap<>();
        map.put("token",jwt);
        //把完整的用户信息存入redis  userid为key 用户信息为value
        redisCache.setCacheObject("login:"+userid,loginUser);
        return new ResponseResult(200,"登录成功",map);
    }

8、退出操作

ServiceImpl:

  /**
     * 退出
     * @return
     */
    @Override
    public ResponseResult logout() {
        //从SecurityContextHolder中的userid
        UsernamePasswordAuthenticationToken authentication  = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
        //
        LoginUser loginUser = (LoginUser)authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        redisCache.deleteObject("login:" + userId);
        return new ResponseResult(200, "注销成功");
    }
本文含有隐藏内容,请 开通VIP 后查看