请选择 进入手机版 | 继续访问电脑版

一种前后端分离架构的权限控制解决方案

[复制链接]
卓小兔 发表于 2021-1-2 19:44:18 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
github
csdn下载
随着互联网的发展,之前的单体架构已经不满意于办理当前的挑战,所以一些企业开始对项目布局举行优化,分布式,微服务等等,这些项目布局的升级确实办理了一部门的问题,但是同样也带来了新的挑战,比如本日博客的主题——安全。安全,是一个随着架构演化,越来越重要的东西。之前的单体应用,大多会采取shiro这个安全框架,而摈弃spring security,因为shiro的功能已经根本上满意企业的要求,而spring security功能全但粗笨,所有一些架构师们认为对项目架构来说是一种负担,但是,当架构演化到分布式以后,存在多个系统时,shiro自己的功能就受到了限制,在笔者的认知里,shiro是基于session来做认证授权的,所以当多个子系统时,session无法共享,固然,也有部门项目采取缓存的方式,把session放到共享缓存中,多个子系统共享这个session,没错,这是办理了当下的问题,但仅仅是当下,因为大概这几个子系统是一个公司的产物,他们可以很方便的使用共享缓存,但是当有别的公司的系统接入时,之前的问题又会再次产生。为了彻底办理这个问题,产生了一种单点登录的办理方案,单点登录中最重要的元素就是token,我本日聊到的办理方案也将用到它,本日我们先不聊单点登录,只讲前后端分离如何包管系统安全。以上言论仅代表笔者个人想法,有差异意见可以相互讨论。
下面进入正题吧,之前的项目是前后一体的,页面也是背景渲染的,所以页面的权限完全的被背景控制,所以权限设计方面比力方便,笔者第一次做一个小项目甚至没有使用任何的权限框架,而是用一个过滤器来控制。不外当你看过框架的原理后,实在发现和我的做法大抵相似,不外是更完备罢了。首先,我先来讲一下大抵的思路:由于系统采取前后端分离,后端与前端只举行数据交互,后端的部门逻辑(包罗权限部门)都交给了前端控制,前端三大框架中Angular就有守卫路由的概念(vue最近刚打仗,但是也看到了类似的东西),有了守卫路由,就能办理部门的权限问题,但是有些细粒度的东西照旧需要详细的信息来控制(比如某个按钮事件,差异的权限大概有差异的展示效果),所以,我们需要在登录时从背景拿到当前用户的所有信息,这个数据可以设置成守卫路由的形式,把守卫路由使用起来,这个数据可以根据系统的安全级别举行加解密等等利用,这些信息可以放在前端的缓存中,比方欣赏器的local storage,移动端可以使用嵌入式数据库sqlite。至此前端的权限控制根本已经实现,但是对于后端来说,前端是不可信的,所以后端的校验也不可缺少,虽然会损耗部门性能,但是为了安全,照旧值得的。
附上项目布局图:

这里我详细先容这个方案中用到的技能栈和工具,因为是体现思路,所以前端采取postman来举行模拟,后端采取的技能栈是springboot,spring security,mybatis,jwt,fastjson,数据库使用的是mysql。附上我的pom文件:
  1.     4.0.0            org.springframework.boot        spring-boot-starter-parent        2.4.1                 com.maochd    security    0.0.1-SNAPSHOT    security    Demo project for Spring Boot            1.8        1.2.73        0.9.0                            org.springframework.boot            spring-boot-starter-web                            org.mybatis.spring.boot            mybatis-spring-boot-starter            2.1.4                            mysql            mysql-connector-java            runtime                            org.projectlombok            lombok            true                            org.springframework.boot            spring-boot-starter-test            test                            org.springframework.boot            spring-boot-starter-security                            io.jsonwebtoken            jjwt            ${jwtt.version}                            com.alibaba            fastjson            ${fastjson.version}                                                    org.springframework.boot                spring-boot-maven-plugin                                                                                        org.projectlombok                            lombok                                                                                    
复制代码
springboot只是一个快速搭建项目的工具,这里我就不外多先容了,spring security 我这里就不讲他的原理了(本人虽然拜读过部门源码,但是照旧有不明白的地方,等我根本明白的时候,我会出一个新的文章来详细的先容),现在我只讲他是如何实现我们的项目的。spring security的核心在于他的过滤链和多个处理惩罚器,我们这里放上他的设置文件:
  1. package com.maochd.security.config;import com.maochd.security.filter.JwtAuthenticationTokenFilter;import com.maochd.security.security.*;import com.maochd.security.service.impl.SelfUserDetailsServiceImpl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpMethod;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/** * security 设置类 * * @author maochd */@Configurationpublic class SpringSecurityConfig extends WebSecurityConfigurerAdapter {    @Autowired    private AjaxAuthenticationEntryPoint authenticationEntryPoint;    @Autowired    private AjaxAuthenticationSuccessHandler authenticationSuccessHandler;    @Autowired    private AjaxAuthenticationFailureHandler authenticationFailureHandler;    @Autowired    private AjaxLogoutSuccessHandler logoutSuccessHandler;    @Autowired    private AjaxAccessDeniedHandler accessDeniedHandler;    @Autowired    private SelfUserDetailsServiceImpl userDetailsService;    @Autowired    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        // 参加自界说的安全认证        // auth.authenticationProvider(provider);        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http            // 去掉 CSRF            .csrf().disable()            // 使用 JWT,关闭token            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)            .and()            .httpBasic().authenticationEntryPoint(authenticationEntryPoint)            .and()            // 过滤所有Options请求            .authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()            // 任何请求,登录后可以访问            .anyRequest()            // RBAC 动态 url 认证            .access("@rbacauthorityservice.hasPermission(request, authentication)")            .and()            // 开启登录            .formLogin()            // 登录乐成处理惩罚器            .successHandler(authenticationSuccessHandler)            // 登录失败处理惩罚器            .failureHandler(authenticationFailureHandler)            .permitAll()            // 默认注销行为为logout            .and().logout().logoutUrl("/logout")            // 退出登录处理惩罚器            .logoutSuccessHandler(logoutSuccessHandler)            .permitAll();        // 记着我        http.rememberMe().rememberMeParameter("remember-me")                .userDetailsService(userDetailsService).tokenValiditySeconds(1000);        // 无权访问处理惩罚器        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);        // JWT过滤器        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);    }}
复制代码
这里我们完全扬弃session,改用jwt,然后加载一下处理惩罚器,这些处理惩罚器我们对他举行重写,来满意自己的需求,每个处理惩罚器我都加上了注释,根本都是返回一个json串,json中包罗各种效果的处理惩罚。
  1. package com.maochd.security.security;import com.alibaba.fastjson.JSON;import com.maochd.security.entity.ResultInfo;import com.maochd.security.enums.ResultEnum;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.web.access.AccessDeniedHandler;import org.springframework.stereotype.Component;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * 无权访问处理惩罚器 * * @author maochd */@Componentpublic class AjaxAccessDeniedHandler implements AccessDeniedHandler {    @Override    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {        httpServletResponse.getWriter().write(JSON.toJSONString(ResultInfo.result(ResultEnum.USER_NO_ACCESS, false)));    }}
复制代码
  1. package com.maochd.security.security;import com.alibaba.fastjson.JSON;import com.maochd.security.entity.ResultInfo;import com.maochd.security.enums.ResultEnum;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.stereotype.Component;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * 未登岸处理惩罚器 * * @author maochd */@Componentpublic class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {        httpServletResponse.getWriter().write(JSON.toJSONString(ResultInfo.result(ResultEnum.USER_NEED_AUTHORITIES, false)));    }}
复制代码
  1. package com.maochd.security.security;import com.alibaba.fastjson.JSON;import com.maochd.security.entity.ResultInfo;import com.maochd.security.enums.ResultEnum;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.authentication.AuthenticationFailureHandler;import org.springframework.stereotype.Component;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * 登录失败处理惩罚器 * * @author maochd */@Componentpublic class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {    @Override    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {        httpServletResponse.getWriter().write(JSON.toJSONString(ResultInfo.result(ResultEnum.USER_LOGIN_FAILED, false)));    }}
复制代码
  1. package com.maochd.security.security;import com.alibaba.fastjson.JSON;import com.maochd.security.entity.ResultInfo;import com.maochd.security.entity.UserInfo;import com.maochd.security.enums.ResultEnum;import com.maochd.security.utils.JwtUtils;import org.springframework.security.core.Authentication;import org.springframework.security.web.authentication.AuthenticationSuccessHandler;import org.springframework.stereotype.Component;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * 登录乐成处理惩罚器 * * @author maochd */@Componentpublic class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {    @Override    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {        UserInfo userDetails = (UserInfo) authentication.getPrincipal();        String jwtToken = JwtUtils.generateToken(userDetails.getUsername(), userDetails.getUserId());        httpServletResponse.getWriter().write(JSON.toJSONString(ResultInfo.result(ResultEnum.USER_LOGIN_SUCCESS, jwtToken, true)));    }}
复制代码
  1. package com.maochd.security.security;import com.alibaba.fastjson.JSON;import com.maochd.security.entity.ResultInfo;import com.maochd.security.enums.ResultEnum;import org.springframework.security.core.Authentication;import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;import org.springframework.stereotype.Component;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * 退出登录处理惩罚器 * * @author maochd */@Componentpublic class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {    @Override    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {        httpServletResponse.getWriter().write(JSON.toJSONString(ResultInfo.result(ResultEnum.USER_LOGOUT_SUCCESS, true)));    }}
复制代码
这里代码大概比力多,根本都是一样的,这里我重点讲一下登录乐成处理惩罚器AjaxAuthenticationSuccessHandler,因为登录利用已经通过,所以我们可以拿到他的用户信息,然后我们需要把用户信息带入新创建的token中,这里我们还可以把权限等信息封装举行,这里我只是演示,所以只有用户名等信息,这里的逻辑可以根据业务重写。
接下来,我们来实现下自界说的认证逻辑
  1. package com.maochd.security.service.impl;import com.maochd.security.dao.RbacAuthorityDao;import com.maochd.security.dao.SystemManagementDao;import com.maochd.security.entity.UserInfo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;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.Component;import java.util.HashSet;import java.util.List;import java.util.Set;/** * security认证实现类 * * @author maochd */@Componentpublic class SelfUserDetailsService implements UserDetailsService {    @Autowired    private RbacAuthorityDao rbacAuthorityDao;    @Override    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        UserInfo user = rbacAuthorityDao.getUser(username);        if (user == null) {            throw new UsernameNotFoundException("该用户不存在");        }        List roles = rbacAuthorityDao.getRoles(user);        Set authoritiesSet = new HashSet();        roles.forEach(role -> authoritiesSet.add(new SimpleGrantedAuthority(role)));        user.setAuthorities(authoritiesSet);        return user;    }}
复制代码
  1. package com.maochd.security.filter;import com.maochd.security.service.impl.SelfUserDetailsServiceImpl;import com.maochd.security.utils.JwtUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;import org.springframework.stereotype.Component;import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * JWT 过滤器 * * @author maochd */@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {    @Autowired    SelfUserDetailsServiceImpl userDetailsService;    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {        String authHeader = request.getHeader("Authorization");        if (authHeader != null && authHeader.startsWith("Bearer ")) {            String authToken = authHeader.substring("Bearer ".length());            String username = JwtUtils.parseToken(authToken, "_secret");            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {                UserDetails userDetails = userDetailsService.loadUserByUsername(username);                if (userDetails != null) {                    UsernamePasswordAuthenticationToken authentication =                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                    SecurityContextHolder.getContext().setAuthentication(authentication);                }            }        }        filterChain.doFilter(request, response);    }}
复制代码
我们先来讲授JwtAuthenticationTokenFilter这个过滤器,他是我们写的代码里的第一道拦截,这里会截取http请求头中的认证信息,方式可以自界说,主要是为了拿到token,如果没拿到,那么就直接进入反面的流程,最后进入用户未登录的处理惩罚器,如果拿到token,我们会对其举行分析,拿到用户名,然后把脚色信息封装到用户实体中,然后对用户实体举行包装后放入security的上下文中,最后进入反面的流程。
SelfUserDetailsService主要是重写security的登录逻辑,我们这里获取了他实时的脚色,并封装到用户实体中,都是为了满意自己的业务。
颠末过滤器后,我们在设置文件里重写了权限校验逻辑,采取动态校验,即实时从数据库中取出权限信息,这里暂时使用数据库,动态的权限信息可以生存在差异介质中,我以为放到缓存中也是个不错的选择,毕竟当请求流量过大时,能减小对数据库的压力。
  1. package com.maochd.security.service.impl;import com.maochd.security.dao.RbacAuthorityDao;import com.maochd.security.entity.UserInfo;import com.maochd.security.service.RbacAuthorityService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.Authentication;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;import java.util.List;/** * security授权实现类 * * @author maochd */@Component("rbacauthorityservice")public class RbacAuthorityServiceImpl implements RbacAuthorityService {    @Autowired    private RbacAuthorityDao rbacAuthorityDao;    @Override    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {        Object userInfo = authentication.getPrincipal();        boolean hasPermission = false;        if (userInfo instanceof UserInfo) {            String username = ((UserDetails) userInfo).getUsername();            List urls = rbacAuthorityDao.getUrlsByUsername(username);            hasPermission = urls.stream().anyMatch(url -> request.getRequestURI().contains(url));        }        return hasPermission;    }}
复制代码
项目中其他的文件都是业务,这里为了方便演示,没有把security部门抽成一个单独的模块,而是采取紧耦合的方式,后期可以把security抽成一个单独的模块,大概与网关等模块举行紧耦合,共同完成对系统的安全包管。业务文件我就不再写出,可以访问我的项目地点,把代码down到当地举行运行。项目工程地点在顶部已经给出。
这里就展示下在postman下的各个情况:
1.未登录

2.登录失败

3. 登录乐成

4.无权访问

5.执行业务乐成


来源:https://blog.csdn.net/qq_35062384/article/details/112065474
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )