zoukankan      html  css  js  c++  java
  • 如何使用Spring Securiry实现前后端分离项目的登录功能

    如果不是前后端分离项目,使用SpringSecurity做登录功能会很省心,只要简单的几项配置,便可以轻松完成登录成功失败的处理,当访问需要认证的页面时,可以自动重定向到登录页面。但是前后端分离的项目就不一样了,不能直接由后台处理,而是要向前端返回相应的json提示。

    在本例的介绍中,主要解决了以下几个问题:

    1.使用json格式数据进行登录。
    2.登录成功或失败处理返回json提示。
    3.未登录时访问需要认证的url时,返回json提示。
    4.session过期时返回json提示。

    一、引入security依赖

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

    二、编写配置文件

    package com.hanstrovsky.config;
    ...
    
    /**
     * @author Hanstrovsky
     */
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true) // security默认不支持注解的方式的权限控制,加上这个注解开启
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final MyUserDetailsService myUserDetailsService;
    
        private final MyPasswordEncoder myPasswordEncoder;
    
        public WebSecurityConfig(MyUserDetailsService myUserDetailsService, MyPasswordEncoder myPasswordEncoder) {
            this.myUserDetailsService = myUserDetailsService;
            this.myPasswordEncoder = myPasswordEncoder;
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 定义加密解密方式
            auth.userDetailsService(myUserDetailsService).passwordEncoder(myPasswordEncoder);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
               	    .httpBasic()
                    // 访问需要认证的url,进行json提示
                    .and().exceptionHandling()
                    .authenticationEntryPoint((req, resp, e) -> {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "未登录或登录超时!");
                        out.write(new ObjectMapper().writeValueAsString(frontResult));
                        out.flush();
                        out.close();
                    })
                    .and()
                    .authorizeRequests()
                    .anyRequest().authenticated()// 必须认证之后才能访问
                
                    .and()
                    .formLogin()// 表单登录
                    .permitAll() // 和表单登录相关的接口统统都直接通过
                
                    .and()
                    .logout().deleteCookies("JSESSIONID")// 注销登录,删除cookie
               // 自定义注销成功,返回json
                    .logoutSuccessHandler(new LogoutSuccessHandler() {
                        @Override
                        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "注销成功!");
                            out.write(new ObjectMapper().writeValueAsString(frontResult));
                            out.flush();
                            out.close();
                        }
                    })
                
                    .and()
                    // session 超时返回json提示
                    .sessionManagement()
                    .maximumSessions(5).maxSessionsPreventsLogin(true)// 同一用户最大同时在线数量5个,超出后阻止登录
                // session 超时返回json提示
                    .expiredSessionStrategy(new SessionInformationExpiredStrategy() {
                        @Override
                        public void onExpiredSessionDetected(
                                SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
                            HttpServletResponse resp = sessionInformationExpiredEvent.getResponse();
                            // 返回提示
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "登录超时!");
                            out.write(new ObjectMapper().writeValueAsString(frontResult));
                            out.flush();
                            out.close();
                        }
                    });
            //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
            http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
    
        //注册自定义的UsernamePasswordAuthenticationFilter,使用json格式数据登录
        @Bean
        CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
            CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
            // 自定义登录成功或失败 返回json提示
            filter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "登录成功!");
                out.write(new ObjectMapper().writeValueAsString(frontResult));
                out.flush();
                out.close();
            });
            filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    String errorMessage = "登录失败";
                    FrontResult frontResult = FrontResult.init(FrontResult.FAILED, errorMessage);
                    out.write(new ObjectMapper().writeValueAsString(frontResult));
                    out.flush();
                    out.close();
                }
            });
            filter.setFilterProcessesUrl("/user/login");
            //重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
            filter.setAuthenticationManager(authenticationManagerBean());
            return filter;
        }
    }
    
    

    三、实现Json登录的处理逻辑

    security默认提供了Basic和表单两种登录方式,不支持Json格式的数据,需要对处理登录的过滤器进行修改。这里,我们重写了UsernamePasswordAuthenticationFilter的attemptAuthentication方法。

    package com.hanstrovsky.filter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Map;
    
    /**
     * 自定义过滤器,重写 attemptAuthentication方法,实现使用json格式的数据进行登录
     *
     * @author Hanstrovsky
     */
    @Slf4j
    public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
                    || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
                ObjectMapper mapper = new ObjectMapper();
                UsernamePasswordAuthenticationToken authRequest = null;
                try (InputStream is = request.getInputStream()) {
                    Map<String, String> authenticationBean = mapper.readValue(is, Map.class);
                    String username = authenticationBean.get("username");
                    String password = authenticationBean.get("password");
                    authRequest = new UsernamePasswordAuthenticationToken(
                            username, password);
                } catch (IOException e) {
                    e.printStackTrace();
                    authRequest = new UsernamePasswordAuthenticationToken(
                            "", "");
                }
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
    
            } else {
                // 保留原来的方法
                return super.attemptAuthentication(request, response);
            }
        }
    }
    
    

    四、实现UserDetailsService接口

    这个接口是用来提供用户名和密码的,可以通过查询数据库获取用户。本例直接在代码中写死。

    package com.hanstrovsky.service;
    
    import com.hanstrovsky.entity.MyUserDetails;
    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.Repository;
    
    /**
     * @author Hanstrovsky
     */
    @Repository
    public class MyUserDetailsService implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
    
            // 可以在此处自定义从数据库查询用户
            MyUserDetails myUserDetail = new MyUserDetails();
            myUserDetail.setUsername(username);
            myUserDetail.setPassword("123456");
            return myUserDetail;
        }
    }
    

    五、实现PasswordEncoder接口

    自定义密码的加密方式。

    package com.hanstrovsky.util;
    
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
    /**
     * 自定义的密码加密方法,实现了PasswordEncoder接口
     *
     * @author Hanstrovsky
     */
    @Component
    public class MyPasswordEncoder implements PasswordEncoder {
    
        @Override
        public String encode(CharSequence charSequence) {
            //加密方法可以根据自己的需要修改
            return charSequence.toString();
        }
    
        @Override
        public boolean matches(CharSequence charSequence, String s) {
            return encode(charSequence).equals(s);
        }
    }
    

    六、实现UserDetails接口

    这个类是用来存储登录成功后的用户数据,security提供了直接获取用户信息的接口

    package com.hanstrovsky.entity;
    
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    
    /**
     * 实现UserDetails,可自定义添加更多属性
     *
     * @author Hanstrovsky
     */
    @Getter
    @Setter
    @Component
    public class MyUserDetails implements UserDetails {
    
        //登录用户名
        private String username;
        //登录密码
        private String password;
    
        private Collection<? extends GrantedAuthority> authorities;
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
    
        private boolean accountNonExpired = true;
    
        private boolean accountNonLocked = true;
    
        private boolean credentialsNonExpired = true;
    
        private boolean enabled = true;
    }
    

    以上,便可以实现前后端分离项目基本的登录功能。

  • 相关阅读:
    前端与算法 leetcode 344. 反转字符串
    JavaScript闭包使用姿势指南
    前端与算法 leetcode 48. 旋转图像
    前端与算法 leetcode 36. 有效的数独
    前端与算法 leetcode 1. 两数之和
    前端与算法 leetcode 283. 移动零
    前端与编译原理 用js去运行js代码 js2run
    前端与算法 leetcode 66. 加一
    前端与算法 leetcode 350. 两个数组的交集 II
    前端与算法 leetcode 26. 删除排序数组中的重复项
  • 原文地址:https://www.cnblogs.com/hanstrovsky/p/12083971.html
Copyright © 2011-2022 走看看