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

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

  • 相关阅读:
    稳扎稳打Silverlight(68) 5.0 XNA 之绘制 3D 图形
    稳扎稳打Silverlight(65) 5.0绑定之通过 ICustomTypeProvider 绑定动态生成的属性
    千呼万唤 HTML 5 (4) 文本语义元素
    千呼万唤 HTML 5 (8) 画布(canvas)之绘制图形
    稳扎稳打Silverlight(66) 5.0其它之自定义 XAML 扩展标记, 通过 XNA 处理声音效果, 支持矢量打印, 统计连击的次数
    千呼万唤 HTML 5 (9) 画布(canvas)之承载媒体
    梦想成真 XNA (2) 绘制精灵,绘制文字
    千呼万唤 HTML 5 (6) 表单元素之 input 元素
    梦想成真 XNA (10) 3D 模型的碰撞检测
    稳扎稳打Silverlight(64) 5.0绑定之 Style 中的 Setter 支持绑定, 绑定父级链上的元素, 隐式指定数据模板, UI 上数据更新的触发方式
  • 原文地址:https://www.cnblogs.com/hanstrovsky/p/12083971.html
Copyright © 2011-2022 走看看