最精简的SpringSecurity6 + 前后端分离实现

最精简的SpringSecurity6 + 前后端分离实现

当前文章收录状态:
未收录

前言

SpringSecurity在SpringMVC中使用比较简单,包含简单的登录、登出页面,会话管理等,但是SpringSecurity如何实现前后端分离项目还不是很熟悉,很多资料讲的很详细,但是不容易理解SpringSecurity和前后端分离结合的重点,因此本文去除所有无关紧要的内容,只保留一个前后端分离和SpringSecurity整合的一个架子,至于其它的代码根据自己的业务自行丰富。

本文基于JWT的前提下,登录之后返回一个JWT字符串,至于不了解JWT的可以自行查找资料,本文也不介绍JWT的生成和解析工作,只使用最简单的字符串表示,重点在于理解整个流程,希望可以帮助到大家。

实现

自定义一个实现UserDetailsService接口的Service,其作用是:Spring Security需要根据用户名查找用户信息,该接口下只有一个方法loadUserByUsername,可以是通过数据库查找用户,也可以使用内存模型,本文只虚拟一个用户,使用固定的用户名和密码:

@Component
public class SecurityService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("执行loadUserByUsername");
User user = new User("admin", "123", AuthorityUtils.NO_AUTHORITIES);
return user;
}
}
@Component
public class SecurityService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("执行loadUserByUsername");
        User user = new User("admin", "123", AuthorityUtils.NO_AUTHORITIES);
        return user;
    }
}
@Component public class SecurityService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("执行loadUserByUsername"); User user = new User("admin", "123", AuthorityUtils.NO_AUTHORITIES); return user; } }

Spring Security根据用户名查找到用户之后会和用户提交的密码做比对,SpringSecurity必须指定一个PasswordEncoder用作密码加密,可以选用DelegatingPasswordEncoder等,本文自己实现一个加密类,直接返回密码本身,不做加密,不然的话在第一步查找用户时还要使用加密的密文。只需要注入bean即可,不需要别的设置:

@Bean
public PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
};
}
@Bean
public PasswordEncoder passwordEncoder(){
    return new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return rawPassword.toString();
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encode(rawPassword).equals(encodedPassword);
        }
    };
}
@Bean public PasswordEncoder passwordEncoder(){ return new PasswordEncoder() { @Override public String encode(CharSequence rawPassword) { return rawPassword.toString(); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encode(rawPassword).equals(encodedPassword); } }; }

在配置类中设置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityFilter securityFilter) throws Exception {
return http
.formLogin(form -> form.disable())//禁用默认登录页面
.logout(config->config.disable())//禁用默认登出页面
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//禁用session,前 后端分离不需要
.httpBasic(httpBasic -> httpBasic.disable())//
.authorizeHttpRequests(
auth -> auth.requestMatchers("/login", "/logout").permitAll()
.anyRequest().authenticated()
)//设置权限,除了登录登出不需要认证,其余均需要认证
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)//添加JWT的处理过滤器,用于从JWT中解析 用户信息
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))//自定义用户认证失败的处理器,否则的话,当用户未认证的情况下,浏览器会直接报出403异常,我们需要的是json提示信息,供前端处理,设置前后效果如下图所示
.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityFilter securityFilter) throws Exception {
    return http
            .formLogin(form -> form.disable())//禁用默认登录页面
            .logout(config->config.disable())//禁用默认登出页面
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//禁用session,前                                                        后端分离不需要
            .httpBasic(httpBasic -> httpBasic.disable())// 
            .authorizeHttpRequests(
                    auth -> auth.requestMatchers("/login", "/logout").permitAll()
                            .anyRequest().authenticated()
                    )//设置权限,除了登录登出不需要认证,其余均需要认证
            .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)//添加JWT的处理过滤器,用于从JWT中解析                                                        用户信息
            .csrf(AbstractHttpConfigurer::disable)
            .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))//自定义用户认证失败的处理器,否则的话,当用户未认证的情况下,浏览器会直接报出403异常,我们需要的是json提示信息,供前端处理,设置前后效果如下图所示
            .build();
}
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityFilter securityFilter) throws Exception { return http .formLogin(form -> form.disable())//禁用默认登录页面 .logout(config->config.disable())//禁用默认登出页面 .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//禁用session,前 后端分离不需要 .httpBasic(httpBasic -> httpBasic.disable())// .authorizeHttpRequests( auth -> auth.requestMatchers("/login", "/logout").permitAll() .anyRequest().authenticated() )//设置权限,除了登录登出不需要认证,其余均需要认证 .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)//添加JWT的处理过滤器,用于从JWT中解析 用户信息 .csrf(AbstractHttpConfigurer::disable) .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))//自定义用户认证失败的处理器,否则的话,当用户未认证的情况下,浏览器会直接报出403异常,我们需要的是json提示信息,供前端处理,设置前后效果如下图所示 .build(); }

自定义AuthenticationEntryPoint之前的效果

图片[1]-最精简的SpringSecurity6 + 前后端分离实现-明恒博客

自定义AuthenticationEntryPoint之后的效果

图片[2]-最精简的SpringSecurity6 + 前后端分离实现-明恒博客

用户未登录情况下的处理器自定义如下:

@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return (request, response, authException) -> {
response.setContentType("text/json;charset=utf-8");
response.getWriter().write("需要登陆");
};
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
    return (request, response, authException) -> {
         response.setContentType("text/json;charset=utf-8");
         response.getWriter().write("需要登陆");
    };
}
@Bean public AuthenticationEntryPoint authenticationEntryPoint(){ return (request, response, authException) -> { response.setContentType("text/json;charset=utf-8"); response.getWriter().write("需要登陆"); }; }

前面设置了验证JWT的过滤器,其作用是用户登录之后会返回一个JWT字符串,下次请求时会带上该字符串,这个过滤器的作用是验证JWT是否正常,如果确实是正确的JWT,则保存用户的登录状态,防止Spring Security认为当前为未认证状态。代码如下:

@Component
public class SecurityFilter extends GenericFilterBean {
@Resource
SecurityService security;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String <a href="https://www.zym88.cn/tag/jwt" title="更多关于 jwt 的文章" target="_blank">jwt</a> = request.getParameter("jwt");//正常情况下,jwt应当放在header里面,示例为了简单直接放在请求参数了
if ("jwtXXX".equals(jwt)){//正常情况下应当验证jwt的正确性和是否过期等,示例为了简单,使用固定的字符串
UserDetails user = security.loadUserByUsername("admin");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
//这一步是核心,将验证信息放入到SecurityContext,表明已经认证成功
SecurityContextHolder.getContext().setAuthentication(token);
System.out.println("jwt正确,认证成功");
}else {
System.out.println("jwt错误,认证失败");
}
chain.doFilter(request, response);
}
}
@Component
public class SecurityFilter extends GenericFilterBean {
    @Resource
    SecurityService security;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String <a href="https://www.zym88.cn/tag/jwt" title="更多关于 jwt 的文章" target="_blank">jwt</a> = request.getParameter("jwt");//正常情况下,jwt应当放在header里面,示例为了简单直接放在请求参数了
        if ("jwtXXX".equals(jwt)){//正常情况下应当验证jwt的正确性和是否过期等,示例为了简单,使用固定的字符串
            UserDetails user = security.loadUserByUsername("admin");
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            //这一步是核心,将验证信息放入到SecurityContext,表明已经认证成功
            SecurityContextHolder.getContext().setAuthentication(token);
            System.out.println("jwt正确,认证成功");
        }else {
            System.out.println("jwt错误,认证失败");
        }
        chain.doFilter(request, response);
    }
}
@Component public class SecurityFilter extends GenericFilterBean { @Resource SecurityService security; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String jwt = request.getParameter("jwt");//正常情况下,jwt应当放在header里面,示例为了简单直接放在请求参数了 if ("jwtXXX".equals(jwt)){//正常情况下应当验证jwt的正确性和是否过期等,示例为了简单,使用固定的字符串 UserDetails user = security.loadUserByUsername("admin"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); //这一步是核心,将验证信息放入到SecurityContext,表明已经认证成功 SecurityContextHolder.getContext().setAuthentication(token); System.out.println("jwt正确,认证成功"); }else { System.out.println("jwt错误,认证失败"); } chain.doFilter(request, response); } }

登录和资源请求控制器:

@RestController
public class LoginController {
@Resource
AuthenticationConfiguration configuration;
@PostMapping("/login")
public String login(String username, String password){
AuthenticationManager authenticationManager = null;
try {
authenticationManager = configuration.getAuthenticationManager();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
Authentication authenticate = authenticationManager.authenticate(token);
//这一步与上面的作用一致
SecurityContextHolder.getContext().setAuthentication(authenticate);
return "登录成功"; //登录成功应该返回json,包含jwt等信息,为了简单,这些都省去了。
} catch (Exception e) {
e.printStackTrace();
return "登录失败";
}
}
@GetMapping("/he")
public String he(){
return "请求的资源";// 表示受保护的资源
}
}
@RestController
public class LoginController {
    @Resource
    AuthenticationConfiguration configuration;

    @PostMapping("/login")
    public String login(String username, String password){
        AuthenticationManager authenticationManager = null;
        try {
            authenticationManager = configuration.getAuthenticationManager();
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
            Authentication authenticate = authenticationManager.authenticate(token);
            //这一步与上面的作用一致
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            return "登录成功"; //登录成功应该返回json,包含jwt等信息,为了简单,这些都省去了。
        } catch (Exception e) {
            e.printStackTrace();
            return "登录失败";
        }
    }

    @GetMapping("/he")
    public String he(){
        return "请求的资源";// 表示受保护的资源
    }
}
@RestController public class LoginController { @Resource AuthenticationConfiguration configuration; @PostMapping("/login") public String login(String username, String password){ AuthenticationManager authenticationManager = null; try { authenticationManager = configuration.getAuthenticationManager(); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); Authentication authenticate = authenticationManager.authenticate(token); //这一步与上面的作用一致 SecurityContextHolder.getContext().setAuthentication(authenticate); return "登录成功"; //登录成功应该返回json,包含jwt等信息,为了简单,这些都省去了。 } catch (Exception e) { e.printStackTrace(); return "登录失败"; } } @GetMapping("/he") public String he(){ return "请求的资源";// 表示受保护的资源 } }

效果演示,使用Postman模拟请求

这是登录成功的结果:

图片[3]-最精简的SpringSecurity6 + 前后端分离实现-明恒博客

登录成功之后,下次请求携带的jwt字符串,就可以访问受保护的资源:

图片[4]-最精简的SpringSecurity6 + 前后端分离实现-明恒博客

如果没登陆,即请求中不包含jwt或者jwt不正确,都会返回错误信息:

图片[5]-最精简的SpringSecurity6 + 前后端分离实现-明恒博客

总结

SpringSecurity6 整合前后端分离还是比较简单的,上面的例子中很多东西都简化了,例如如何生成、验证jwt,控制器返回的json字符串,还有用户信息通常需要从数据库中读取,这些在我看来都是具体的业务性的内容, 只要这个整合的架子搭起来之后,其它的内容同学们可以根据自己的需要进行修改。

© 版权声明
THE END
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=270198dipw4ko
点赞7赞赏 分享
I can accept failure but I can’t accept not trying.
可以接受暂时的失败,但绝对不能接受未曾奋斗过的自己
评论 抢沙发

请登录后发表评论

    暂无评论内容