SpringSecurity实现自定义登录接口

SpringSecurity实现自定义登录接口

1、配置类 ConfigClazz(SpringSecuriey的)
java 复制代码
    //首先就是要有一个配置类
	@Resource
    private DIYUsernamePasswordAuthenticationFilter diyUsernamePasswordAuthenticationFilter;

    /*SpringSecurity配置*/
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(
                    authorize -> authorize
                            .requestMatchers("/user/**","/").hasRole("user") //拥有user的角色可访问的接口
                            .requestMatchers("/manager/**").hasRole("manager")//拥有manager的角色可访问的接口
                            .requestMatchers("/login/**").permitAll()
            
                            .anyRequest() 
                            .authenticated() // 任何请求都需要授权,重定向到
            );

        /*登录页*/
		http.formLogin(AbstractHttpConfigurer::disable);//禁用默认的登录接口,使用自定义的登录接口

        /*登出*/
        http.logout(logout ->{
            logout
                    .logoutUrl("/goOut").permitAll()
                    //登录退出成功,向前端返回json格式的字符串
                    .logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication)->{
                        Map<String, String[]> parameterMap = request.getParameterMap();
                        //进入登录页时,判断是否已经登陆过 TowLogin 参数
                        if(!parameterMap.isEmpty() && parameterMap.get("TowLogin")[0].equals("true")){
                            String json = JSON.toJSONString(Code.NOTowLogin);
                            response.setContentType("application/json;charset=UTF-8");
                            response.getWriter().println(json);
                        } else {
                            String json = JSON.toJSONString(Code.SuccessLogout);
                            response.setContentType("application/json;charset=UTF-8");
                            response.getWriter().println(json);
                        }
                    });
        });

        /*向过滤器链中添加自定义的过滤器
          用自定义的过滤器代替 UsernamePasswordAuthenticationFilter 过滤器
        */
        http.addFilterAfter(diyUsernamePasswordAuthenticationFilter, LogoutFilter.class);

        /*请求异常处理*/
        http.exceptionHandling(exception ->{

            /*用户未登录时,访问限权接口,返回 json 格式的字符串
            这个配是。把页面跳转交给前端,即:用户未登录时,后端只返回 json 格式的字符串,不会跳转页面
            -- 未登录时,重定向的 url  .loginPage("/login/getLoginHTML").permitAll(),就不起作用了 --
            */
            exception.authenticationEntryPoint((HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)->{
                String json = JSON.toJSONString(Code.NoLogin);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
            });

            //响应登录用户访问未授权路径时(user角色访问manager角色的接口) 有 未授权 json 提示
            exception.accessDeniedHandler((HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)->{
                String json = JSON.toJSONString(Code.Forbidden);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().println(json);
            });
        });


        /*会话管理*/
        http.sessionManagement(session -> {
            session
                    //表示,最大连接数量为 1 ,同一个账号,最多只能在一台设备上登录,当第二个登陆时,会把第一个挤掉
                    .maximumSessions(1)
                    //挤掉后,对前端返回的json字符串
                    .expiredSessionStrategy((SessionInformationExpiredEvent event)->{
                        String json = JSON.toJSONString(Code.ForeignLogin);
                        HttpServletResponse response = event.getResponse();
                        response.setContentType("application/json;charset=UTF-8");
                        response.getWriter().println(json);
                    });
        });

        /*开启跨域访问*/
        http.cors(withDefaults());

       /* 禁用csrf的防御手段。
        * 开启后,相当于每次前端访问接口的时候
        * 都需要携带_crsf为参数名的参数,功能类似于 token,
        * 因此建议禁用
        * */
        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

	//设置密码的编码方式(必须有)
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder(10); 
    }
  • 解释 _scrf 在哪看,只有最初有,后面就没有,但是如果不携带,就不让你访问接口,因此建议禁用
2、DIYUsernamePasswordAuthenticationFilter
  • 该类用于替换 UsernamePasswordAuthenticationFilter 过滤器,应用自己自定义的过滤器
java 复制代码
@Component  //相当于 UsernamePasswordAuthenticationFilter
public class DIYUsernamePasswordAuthenticationFilter extends OncePerRequestFilter {

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        /*问题:不能读取请求体中的信息,因为是一次性的,读完,后面就不能用了
        * 因此,这里避免用json格式传输 账号 和 密码
        * */

        //获取非 json 格式传输的,OK了,只要前端给 json 格式 的token就能获取了
        Map<String, String[]> parameterMap = request.getParameterMap(); //有前端打开

        SUser user = null;
        HttpSession session = request.getSession();

        //有前端打开
        //检查token,通过token解析出用户的账号,根据账号,从 session 中查询
        if(parameterMap.get("token") != null)
            user = (SUser)session.getAttribute(parameterMap.get("token")[0]);

        if (user == null) {
            //放行,表示已经退出,需要重新验证,区别就是有没有 存入SecurityContextHolder 一步骤
            filterChain.doFilter(request, response);
            return;
        }

        //存入SecurityContextHolder,获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =  // 没有前端获取用户数据目前先这样写
                new UsernamePasswordAuthenticationToken(user,user.getPassword(),user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //验证成功,放行
        filterChain.doFilter(request, response);
    }
}
3、DIYAuthenticationProvider
  • 该类是发放授权的接口
java 复制代码
@Component
public class DIYAuthenticationProvider implements AuthenticationProvider {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        // 从数据库中加载用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        // 检查密码是否正确
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("用户名或密码错误");
        }

        // 创建一个已认证的 Authentication 对象
        UsernamePasswordAuthenticationToken authenticatedToken =
                new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        authenticatedToken.setDetails(authentication.getDetails());
        return authenticatedToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

4、DIYAuthenticationManager

  • 该类是用来调用发放授权接口
java 复制代码
@Component
public class DIYAuthenticationManager implements AuthenticationManager {

    @Resource  //这里虽然是注入的接口,但是由于自定义的类 DIYAuthenticationProvider 实现了该接口,因此优先使用
    AuthenticationProvider authenticationProvider;

      		  //这里其实可以调用默认的 授权提供者,有匹配的就会授权,但是,没必要,因为肯定匹配不了,最后还是用自己的,
    @Override //那不如  直接就用自己的就好了
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return authenticationProvider.authenticate(authentication);
    }
}
5、MySQLUserDetailsManager
  • 该类用于获取用户的信息
java 复制代码
@Component  //将这个类交给Spring容器管理,即:创建该类的 bean 对象,进而取代(重写)原来的方法
public class MySQLUserDetailsManager implements UserDetailsService{
    //由于是基于数据库的,因此,只需要实现一个 UserDetailsService 接口就好,不需要实现其他的接口

    @Resource //这个是Mapper接口,用于从数据库中调用查询信息
    SUserMapper sUserMapper; 
    @Resource  //这个是必要的
    HttpServletRequest request; 

    @Override                           //String username
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        //获取数据信息要在这里开始,由于只暴露用户输入account,因此数据库中的数据只能,所有的 account都不一样,才能唯一匹配 account,这里 Email 一定不一样
        //这里的 username 就是用户输入的账号,为了方便,就换一个变量名 account
        List<SUser> sUsers = sUserMapper.selectAllByEmail(account);  //这里 Email 一定不一样
        if(sUsers != null && !sUsers.isEmpty()) {
            SUser sUser = sUsers.get(0);

            //这里把 authenticate 这个用户的信息存到session中,如果调用退出登录接口,就会删除session里面的内容
            HttpSession session = request.getSession();
            session.setAttribute(String.valueOf(sUser.getEmail()),sUser);

            return sUser;
        } else {
            throw new UsernameNotFoundException(account);
        }
    }
}
6、控制层
java 复制代码
@Controller
@Tag(name = "登录注册")
@RequestMapping("/login")
public class LoginController {

    @Resource
    private SUserService sUserService;
    @Resource
    private AuthenticationManager authenticationManager;

    @GetMapping("/getLoginHTML")  //进入登录页的接口
    public String getLoginHtml(HttpSession session){
        boolean aNew = session.isNew();
        if(aNew)
            return "login";
        //如果一个浏览器试图登录两次,那么就会直接调用退出接口
        return "redirect:/goOut?TowLogin=true";
    }
    
    
    
    @PostMapping("/ooo")  //由于是自定义登录接口,因此什么请求都可以,建议用Post
    @ResponseBody  //将返回值写入响应体中
    public Code login(String account,String password){
        SUser sUser = new SUser();
        sUser.setEmail(account);
        sUser.setPassword(password);
        
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sUser,password);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
 
        if(Objects.isNull(authenticate))
            throw new AuthenticationCredentialsNotFoundException("用户账号或密码错误");
        else{
            //这里响应回去一个 token,根据账号加密后,生成的 token
            Map<String, String> map = new HashMap<>();
            map.put("token",authenticate.getName());
            return new Code<>(Code.OK, map);
        }
}
7、增强用户的实体类
  • 这里由于要封装用户的详细信息,而用 MybatisX 生成的 User 实体类不能满足需求,因此要实现一个接口
java 复制代码
@TableName(value ="s_user")
@Data
@Repository  //将这个类交给IOC容器(Spring)管理
public class SUser implements Serializable , UserDetails{  //实现这个接口
    /**
     * 主键id,自动递增
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 用户名:<=10
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 性别:女 , 男
     */
    private String sex;

    /**
     * 邮箱账号:<=30
     */
    private String email;

    /**
     * 密码:<=15
     */
    private String password;

    /**
     * 是否被禁用:0-未禁用,1-已禁用
     */
    private Integer isForbidden;

    /**
     * 该账号的角色:0-普通用户,1-管理员
     */
    private String role;

    /**
     * 是否被删除(或用户注销):0-未删除,1-删除
     */
    @TableLogic
    private Integer isDelete;

    @Serial
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        /*这里要自己拼接 ROLE_ + role
        * ROLE_ : 是固定的
        * 由于我这里的实体类设计的是:String role; 不是数组形式,因此不用循环
        * 如果是数组形式的限权,循环遍历,并创建 SimpleGrantedAuthority 就好了
        * */
        List<SimpleGrantedAuthority> list  = new ArrayList<>();
        list.add(new SimpleGrantedAuthority("ROLE_" + role));
        return list;
    }

    @Override  //注意:这里的用户名是 账号
    public String getUsername() {
        return this.email;
    }

    @Override//没有这个设定就返回通过的结果,可以用翻译 isAccountNonExpired ? 在每个方法名后加一个? 问自己是true/false
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override//自己的实体类中有这个设定,就返回判断的结果
    public boolean isAccountNonLocked() {
        return isForbidden == 1;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
7、依赖
  • java版本 17
  • springBoot版本 3.2.0
xml 复制代码
	<dependencies>

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

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

        <!--SpringSecurity依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--thymeleaf作为视图模板-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--mybatis-Puls的依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.4.1</version>
            <!--由于SpringBoot的版本太高,需要这样1-->
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--由于SpringBoot的版本太高,需要这样2-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>

        <!--mysql的驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

        <!--简化实体类开发-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--JavaWeb组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--引入json数据依赖,用于给前端返回json类型的数据-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.37</version>
        </dependency>

        <!--knife4j测试,对请求的测试,有两种,swagger-ui.html / doc.html 都可以-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.4.0</version>
        </dependency>

    </dependencies>
相关推荐
今天不coding20 分钟前
自定义持久层框架------从零到一手写一个mybatis
java·mybatis
码农阿岩~24 分钟前
适合初学者的[JAVA]: 服务框架常见问题
java·开发语言
小羊在奋斗1 小时前
【C++】二叉搜索树+变身 = AVL树
java·开发语言·c++·机器学习
疯一样的码农2 小时前
Spring Boot Starter Parent介绍
java·spring boot·后端
iQM752 小时前
Spring Boot集成RBloomFilter快速入门Demo
java·spring boot·spring
爱上语文2 小时前
Springboot 阿里云对象存储OSS 工具类
java·开发语言·spring boot·后端·阿里云
代码代码快快显灵5 小时前
java之异常处理
java·开发语言
茶馆大橘5 小时前
Spring Validation —— 参数校验框架
java·后端·学习·spring
阿望要努力上研究生7 小时前
若依项目搭建(黑马经验)
java·redis·node.js·maven·管理系统
一只脑洞君7 小时前
Kubernetes(K8s)的简介
java·容器·kubernetes