Springboot + Spring Security +JWT + mybatis plus 实现前后端分离登录认证及权限控制

前言:
参考文章:

  1. Springboot + Spring Security 实现前后端分离登录认证及权限控制
  2. springboot+Spring-Security+JWT 实现用户登录和权限认证

项目主要代码来源于参考的第一篇文章,jwt来源于第二篇
改变:

  1. 将第一篇文章的项目改成我熟悉的无xml文件模式
  2. 将第二篇文章的jwt加入第一篇文章代码
  3. 代码部分修改…

项目结构
项目结构

一.基于权限控制

  1. 编写controller接口
    a.管理员资源接口,需要有管理员相关权限或角色才可以访问
/**
 * @description: 管理员资源接口,需要有管理员相关权限或角色才可以访问
 * @author: ※狗尾巴草
 * @date: 2020-11-14 18:09
 **/
@RestController
@RequestMapping("admin")
public class adminController {


    @GetMapping("info")
    public String getCommon(){
        return "admin资源";
    }


}

b.普通用户资源接口,需要有普通相关权限或角色才可以访问

/**
 * @description: 普通用户资源接口,需要有普通相关权限或角色才可以访问
 * @author: ※狗尾巴草
 * @date: 2020-11-13 12:43
 **/
@RestController
@RequestMapping("user")
public class userController {

    @GetMapping("getUser")
    public String getUser(){
        return "getUser";
    }


}

c.公共资源接口,无需权限或绝便可访问

/**
 * @description: 公共资源接口,无需权限或绝便可访问
 * @author: ※狗尾巴草
 * @date: 2020-11-13 14:29
 **/
@RestController
@RequestMapping("common")
public class commonCOntroller {


    @GetMapping("info")
    public String getCommon(){
        return "公共资源";
    }


}
  1. 在数据库添加接口以及权限相关数据
    a.接口,sys_request_path表
    接口数据
    b.角色,sys_role表
    角色数据
    c.权限
    权限信息

  2. 登录成功处理器,CustomizeAuthenticationSuccessHandler

添加JWT,将t权限信息保存到oken中,待会验证时再取出权限进行鉴权

/**
 * @description: 登录成功处理器
 * @author: ※狗尾巴草
 * @date: 2020-11-13 14:19
 **/
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //更新用户表上次登录时间、更新人、更新时间等字段
        User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        SysUserEntity sysUser = sysUserService.selectByName(userDetails.getUsername());
        sysUser.setLast_login_time(new Date());
        sysUser.setUpdate_time(new Date());
        sysUser.setUpdate_user(sysUser.getId());
        sysUserService.updateById(sysUser);

        //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
        //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展

        //  获取用户权限
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        ArrayList<String> list = new ArrayList<>();
        String role = "";
        for (GrantedAuthority authority : authorities){
            role += authority.getAuthority()+",";
            list.add(authority.getAuthority());
        }
        role=role.substring(0,role.length()-1);
        //将权限放入token中
        String token = JwtTokenUtils.createToken(userDetails.getUsername(), role);
        sysUser.setToken(JwtTokenUtils.TOKEN_PREFIX+token);

        sysUser.setAuthorities(list);
        SysUserVO userVO = ConvertUtils.sourceToTarget(sysUser, SysUserVO.class);

        //返回json数据
        JsonResult result = ResultTool.success();
        result.setData(userVO);


        //处理编码方式,防止中文乱码的情况
        httpServletResponse.setContentType("text/json;charset=utf-8");
        //塞到HttpServletResponse中返回给前台
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}
  1. 访问决策管理器,CustomizeAccessDecisionManager

原来的代码暂无改变

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> iterator = collection.iterator();
        //当前用户所具有的权限
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        //当前请求需要的权限
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        //抛出AccessDeniedException异常,下面会新建个方法去捕获异常
        throw new AccessDeniedException("权限不足!");
    }
  1. 新添加AccessDeniedException权限异常捕获,CustomAccessDeniedHandler
/**
 * @description: 捕获 AccessDeniedException异常
 * @author: ※狗尾巴草
 * @date: 2020-11-22 0:15
 **/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        JsonResult fail = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().write(JSON.toJSONString(fail));
    }
}
  1. 关于JWT

改变
删除原有的
// 如果请求头中没有Authorization信息则直接放行了 if (tokenHeader == null || !tokenHeader.startsWith(TestJwtUtils.TOKEN_PREFIX)) { chain.doFilter(request, response); return; }
这样就可以每个接口都取读取token,
修改getAuthentication方法,在getAuthentication方法解析token,取出权限或角色等信息


/**
 * @description: 鉴权操作
 * @author: ※狗尾巴草
 * @date: 2020-11-14 0:20
 **/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);

        // 对请求头d的token,进行解析,获取用户权限
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    // 从token中获取用户权限
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        // token为空或不正确,解析异常
        try {
            String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
            String username = JwtTokenUtils.getUsername(token);
            String roles = JwtTokenUtils.getUserRole(token);
            List<GrantedAuthority> grantedAuthorities  = new ArrayList<>();
            for(String role: roles.split(",")){
                if(StringUtils.isNotBlank(role)){
                    grantedAuthorities.add(new SimpleGrantedAuthority(role));
                }
            }
            if (username != null){
                return new UsernamePasswordAuthenticationToken(username, null,grantedAuthorities);
            }
        }catch (Exception e){
            return null;
        }
        return null;
    }
}


  1. 关于配置

改变:
注释sesison

 //.maximumSessions(1)//同一账号同时登录最大用户数
//.expiredSessionStrategy(sessionInformationExpiredStrategy)//会话信息过期策略会话信息过期策略(账号被挤下线)

添加禁用session代码

// 不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        

添加拦截器

.addFilter(new JWTAuthorizationFilter(authenticationManager()))

注释掉会话信息过期策略CustomizeSessionInformationExpiredStrategy这个类
完整配置代码


/**
 * @description: Security配置
 * @author: ※狗尾巴草
 * @date: 2020-11-12 12:25
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomizeAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private CustomizeAuthenticationSuccessHandler successHandler;

    @Autowired
    private CustomizeAuthenticationFailureHandler failureHandler;

    @Autowired
    private CustomizeLogoutSuccessHandler logoutSuccessHandler;

//    @Autowired
//    private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;

    @Autowired
    private CustomizeAccessDecisionManager accessDecisionManager;

    @Autowired
    private CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;


    @Autowired
    CustomAccessDeniedHandler accessDeniedHandler;

//    @Autowired
//    private CustomizeAbstractSecurityInterceptor securityInterceptor;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 设置默认的加密方式(强hash方式加密)
        return new BCryptPasswordEncoder();
    }


    @Bean
    public UserDetailsService userDetailsService() {
        //获取用户账号密码及权限信息
        return new UserDetailsServiceImpl();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置认证方式
        auth.userDetailsService(userDetailsService());
    }

//    @Override
//    public void configure(WebSecurity web) {
//        //对于在header里面增加token等类似情况,放行所有OPTIONS请求。
//        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
//    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //http相关的配置,包括登入登出、异常处理、会话管理等
        http
        .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
                        o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
                        return o;
                    }
                })
//                .antMatchers("/user/**").hasAuthority("query_user")
                //基于角色控制
//                .antMatchers("/admin/**").hasAnyRole("admin")
//                .antMatchers("/user/**").hasAnyRole("user")
        //登入
        .and()
                .formLogin()
                .loginPage("/auth/login")
                .permitAll()//允许所有用户
                .successHandler(successHandler)//登录成功处理逻辑
                .failureHandler(failureHandler)//登录失败处理逻辑
//        //登出
        .and().logout()
                .permitAll()//允许所有用户
                .logoutSuccessHandler(logoutSuccessHandler)//登出成功处理逻辑
                .deleteCookies("JSESSIONID")//登出之后删除cookie
                //异常处理(权限拒绝、登录失效等)
        .and().exceptionHandling().
                authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
                .accessDeniedHandler(accessDeniedHandler)//异常捕获
//                .authenticationEntryPoint(new JWTAuthenticationEntryPoint())
//        限制同一账号只能一个用户使用  会话管理
        .and().sessionManagement()
//                .maximumSessions(1)//同一账号同时登录最大用户数
//                .expiredSessionStrategy(sessionInformationExpiredStrategy)//会话信息过期策略会话信息过期策略(账号被挤下线)
        ;
        http.csrf().disable().cors()
                .and()
//                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        ;
//        http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中
    }





}

  1. 添加两个用户
    user

  2. 测试
    一:未登录状态

    a.管理员资源接口:http://localhost:8666/admin/info
    admin接口

    其他接口一样…

    二:登录获取token

    a.登录 localhost:8666/auth/login?username=user&password=123456

登录

b 已经点击过登录,但不携带token进行访问,被拦截

已经点击过登录,但不携带token进行访问

以下携带token进行访问

c.访问user接口,http://localhost:8666/user/getUser,并成功返回数据

user接口

d.用普通用户的token访问管理员接口,http://localhost:8666/admin/info,并成功进行拦截

admin接口
三:公共接口以及管理员接口同理,不做操作了
四:流程分析
1.携带token访问,就本项目而言,所写配置中,先被JWTAuthorizationFilter拦截器进行拦截,取出token中的权限信息,并返回UsernamePasswordAuthenticationToken对象,执行super.doFilterInternal(request, response, chain);
token拦截
2 .查询接口所配置的权限,CustomizeFilterInvocationSecurityMetadataSource

查询接口所配置的权限

3.然后到访问决策管理器CustomizeAccessDecisionManager ,对url所需要的权限和用户所具有的权限进行决策.具有权限就通过,权限不足便抛出异常权限决策
4.然后回到JWTAuthorizationFilter拦截器JWTAuthorizationFilter
省略一堆源码…最后访问到所需资源user资源
二:基于角色控制
注:角色和权限共用GrantedAuthority接口,不同的是角色多了个前缀"ROLE_"

1.修改UserDetailsServiceImpl,登陆成功后,取出该用户所有的角色,并注释权限的相关代码

        //获取用户所有角色
        List<SysRoleEntity> roleList = sysRoleService.selectByUser(userEntity.getId());
        // 声明用户角色
        roleList.forEach(role ->{
            //基于角色控制
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRole_code()));
        });

2.SecurityConfig配置路由

//基于角色控制,这里的角色名不需要加前缀,因为在hasAnyRole方法的源码里面已经写好拼接了
.antMatchers("/admin/**").hasAnyRole("admin")
.antMatchers("/user/**").hasAnyRole("user")

3.注释权限配置代码,否则还是会以权限控制方式进行鉴权

.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    @Override
    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
        o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
        o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
        return o;
    }
})

a.进行登录,localhost:8666/auth/login?username=user&password=123456,获取token

登录

携带token进行访问user接口,http://localhost:8666/user/getUser

user

携带token进行访问admin接口,http://localhost:8666/admin/info,并成功被拦截

admin接口
注: 在参考的第一篇文章中,那个权限拦截器已经被我注销掉了,经过几次断点调试发现,他会在权限拦截器访问决策管理器 循环三次,重复读取数据库数据,三次过后就正常访问资源,思来想去也想不明白,于是就被我删除了

最后,
项目地址

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值