自学内容网 自学内容网

SpringSecurity实现自定义登录接口

SpringSecurity实现自定义登录接口

1、配置类 ConfigClazz(SpringSecuriey的)
    //首先就是要有一个配置类
@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 过滤器,应用自己自定义的过滤器
@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
  • 该类是发放授权的接口
@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

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

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

        //这里其实可以调用默认的 授权提供者,有匹配的就会授权,但是,没必要,因为肯定匹配不了,最后还是用自己的,
    @Override //那不如  直接就用自己的就好了
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return authenticationProvider.authenticate(authentication);
    }
}
5、MySQLUserDetailsManager
  • 该类用于获取用户的信息
@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、控制层
@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 实体类不能满足需求,因此要实现一个接口
@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

<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>

原文地址:https://blog.csdn.net/practice_warm/article/details/142718106

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!