美文网首页单车
单车第五天

单车第五天

作者: shenyoujian | 来源:发表于2018-10-01 14:51 被阅读20次

    转自http://coder520.com/
    1、修改用户信息
    先想一个问题,要修改用户信息得先从移动端传递用户id过来,之后才能进行获取用户然后进行修改,但是如果移动端出错了,本来01想修改用户名但是它出错传递了个03过来,然后我服务端修改了03的信息,这就不好了吧。这种修改信息的,传递的id最好从后台获取。不然不安全。
    解决办法:从后台获取id,可以从token获取这个id,而token可以从移动端放入headers传递过来。可以先这样做,写一个baseController里面写一个getCurrent方法,这个方法的作用就是从token里获取用户id。

    2、controller,注意我们与移动端约定好用json传输,所以不能使用String类型参数,必须使用requestbody把json封装成对象。

    public ApiResult modifyUsername(@RequestBody User user){
            ApiResult resp = new ApiResult();
            try {
                userService.modifyUsername(user);
            } catch (MaMaBikeException e) {
                //校验失败
                log.error(e.getMessage());
                resp.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
                resp.setMessage(e.getMessage());
            } catch (Exception e) {
                // 登录失败,返回失败信息,就不用返回data
                // 记录日志
                log.error("Fail to modifyUsername", e);
                resp.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
                resp.setMessage("内部错误!");
            }
    
            return resp;
        }
    

    service

    /**
         * Author ljs
         * Description 修改用户信息业务
         * Date 2018/9/25 16:30
         **/
        @Override
        public void modifyUsername(User user) throws MaMaBikeException{
            userMapper.updateByPrimaryKeySelective(user);
        }
    

    3、上面的controller是不完善的,因为user里只有newusername和headers里的token(约定移动端不能传递userid),用户id由我们后端自己获取。定义获取用户的方法,因为这个方法会被多处用到,所以放在basecontroller里。因为只允许继承的类使用,所以方法定义为protected好一点。

    public class BaseController {
    
        @Autowired
        private CommonCacheUtil redis;
    
        /**
         * Author ljs
         * Description 根据token获取user
         * Date 2018/9/25 20:10
         **/
        protected UserElement getCurrenUser(){
            //1、使用springmvc提供的类去获取request
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
            //2、从header里获取token
            String token = request.getHeader(Constants.REQUEST_TOKEN_KEY);
            if (!StringUtils.isBlank(token)) {
                //3、不为空,根据token去redis获取用户
                try{
                    UserElement ue = redis.getUserByToken(token);
                    return ue;
                }catch (Exception e){
                    return null;
                }
            }
            return null;
        }
    }
    

    创建getUserByToken方法

    /**
         * Author ljs
         * Description 根据token获取缓存的用户
         * Date 2018/9/25 21:23
         **/
        public UserElement getUserByToken(String token) throws MaMaBikeException {
            UserElement ue = null;
            JedisPool pool = jedisPoolWrapper.getJedisPool();
            if (pool != null) {
                //1.7支持try()括号里的内容在try之后自动关闭流或者资源,不用自动关闭
                try (Jedis jedis = pool.getResource()) {
                    jedis.select(0);
                    //根据key从redis获取Map
                    try {
                        Map<String, String> map = jedis.hgetAll(TOKEN_PREFIX + token);
                        if (!CollectionUtils.isEmpty(map)) {
                            //把map转对象
                            ue = UserElement.fromMap(map);
                        }else{
                            log.warn("Fail to find cached element for token {}", token);
                        }
    
                    } catch (Exception e) {
                        log.error("Fail to get token from redis", e);
                        throw new MaMaBikeException("Fail to get token content");
                    }
                }
            }
            return ue;
        }
    

    4、最后完善controller,就可以根据id去更新用户的信息了。添加headers和传递要修改的新名字

     //根据token获取用户id
                UserElement ue = getCurrenUser();
                user.setId(ue.getUserId());
                userService.modifyUsername(user);
    

    启动服务器测试


    image.png
    image.png image.png

    ok。

    5、登录方法是不用拦截,而修改个人信息是需要拦截的,现在我们用不了之前通过url来拦截,我们这是对接app的。所以我们可以通过判断token是否正确来进行拦截。

    6.1、整合springsecurity,加入pom

    <!--整合springSecurity-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    

    springsecurity教程
    spring4all http://www.spring4all.com/article/428
    spring官方文档https://docs.spring.io/spring-security/site/docs/4.2.8.RELEASE/reference/htmlsingle/

    6.2、创建配置类

    /**
     * @Author ljs
     * @Description security配置类
     * @Date 2018/9/28 16:33
     **/
    
    @Configuration              //springboot启动时加载该配置类
    @EnableWebSecurity          //启动springsecurity的web安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        //web安全配置的细节,如定义哪些url路径应该被保护,哪些不应该。
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //关闭默认打开的crsf保护
           http.csrf().disable()
                   //允许含login不需要身份验证
                   .authorizeRequests()
                   .antMatchers("/**/login").permitAll()
                   //其他请求都需要身份验证
                   .anyRequest().authenticated()
                   .and()
                   //创建成无状态的请求,即不创建session,因为我们是和移动端对接的。
                   .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                   ;
        }
    }
    
    
    • 注意:crsf跨站脚本攻击,就是那些钓鱼网站,如果我的网站有一个表单需要用户提交,他们就仿造我这个表单去欺骗用户填写信息然后请求我的表单。解决办法就是给表单添加隐藏域字段,该字段由服务端生成一个唯一的标识,然后每一个请求都判断是否有该字段,并且该标识是否正确。
    • 然后debug,请求modeifyusername,可以看到


      image.png

      而login是可以访问的到,ok测试成功。
      6.3、上面我们只是完成了拦截,接下来需要校验token,当token正确的时候,可以让他除login之外的请求通过。

    • 认证+授权的流程
      大致:
      filter->manager->provider(一个或N个提供权限信息) entrypoint(统一异常处理,前面三个只要有每一个出错都会来到这里)


      image.png

    具体:
    自定义filter实现抽象类PreAuthenticatedProcessingFilter的getPreAuthenticatedPrincipal方法,该方法是提取用户提交需要校验的信息,然后传递给自定义的provider实现AuthenticationProvider的supports方法,该方法是校验filter传递过来的对象,校验成功返回true,接着调用Authentication进行授权。其中filter提取不到,provider校验失败等都会调用自定义的entrypoint实现AuthenticationEntryPoint的commence方法告诉客户端校验失败。

    最后把自定义的filter,provider,entrypoint配置到security的配置类里。
    因为provider可以有多个,需要一个叫manager的List来管理

    //往manager里添加provider
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(new RestAuthenticationProvider());
        }
    

    那么现在filter就不是调用provider而是manager,需要给filter设置manager

    //给filter里设置manager
        private RestPreAuthenticatedProcessingFilter getPreAuthenticatedProcessingFilter() throws Exception {
            RestPreAuthenticatedProcessingFilter filter = new RestPreAuthenticatedProcessingFilter();
            filter.setAuthenticationManager(this.authenticationManagerBean());
            return filter;
        }
    

    configure配置filter和entrypoint

    .and().httpBasic().authenticationEntryPoint(new RestAuthenticationEntryPoint())
    .and().addFilter(getPreAuthenticatedProcessingFilter())
    

    debug接下来验证流程:
    请求modifyusername


    image.png

    接着进到抽象类里方法,该方法用于判断提取的信息是否为空


    image.png
    因为我们return的是null,所以毫无疑问来到entrypoint
    image.png
    接下来验证login

    同样是111111之后就来到了controller而不会到entrypoint,因为我们在配置类里声明了login允许访问。


    image.png
     .antMatchers("/**/login").permitAll()
    

    ok,认证授权流程验证成功。

    7、写活无需拦截的url
    我们把无需拦截的url写在代码不友好,后续如果要添加其他无需拦截的url,还得修改代码,继续添加,我们可以写在一个配置文件里,不要写在yml里,因为yml识别不了**,写在properties

     .antMatchers("/**/login").permitAll()
    

    7.1、parameter.properties文件,以后还有其他无需拦截就再后面添加就行了。

    #security无需拦截的url
    security.noneSecurityPath=/**/login
    

    7.2、加载properties文件

    @PropertySource(value = "classpath:parameter.properties")
    

    7.3、读取properties文件,因为antMatchers需要传递一个String类型的列表

    @Value("#'${security.noneSecurityPath}'.split(',')")
    private List noneSecurityPath;
    

    7.4、为了能使用split(',')和占位符,再加载的时候需要进行配置

    /**
         * 用于properties文件占位符解析
         * @return
         */
        @Bean
        public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
            return new PropertySourcesPlaceholderConfigurer();
        }
    

    7.5、最后在security配置类注入Parameters类,并且读取noneSecurityPath,把读取的List对象转换为String数组传进antMatchers

     //注入Parameter类
        @Autowired
        private Parameters parameters;
    
    
    .antMatchers((String[])parameters.getNoneSecurityPath().toArray(new String[parameters.getNoneSecurityPath().size()])).permitAll()//符合条件的路径放过验证
    

    7.6、最后debugger测试login可以通过,ok

    8、处理无需验证的请求
    上面已经验证了认证授权的流程,还写活了无需拦截的url,当我们login请求之后,filter同样会拦截下来,但是我们返回null,所以它还是会进去方法判断,然后打出日志意思就是从这个请求中获取不到任何验证的信息

    16:24 DEBUG [c.l.m.s.RestPreAuthenticatedProcessingFilter] No pre-authenticated principal found in request
    

    我们需要给这些无需验证直接放过的请求做特殊处理,
    解决:判断url是否是无需验证的url,是就随便给一个权限然后让它们通过,不然不会调用接下来的provider,打印错误日志造成逻辑不通。

    8.1、首先filter需要知道哪些url是无需验证的,我们得先注入parameter,但是filter这个是独立于spring容器,不能使用getset注入,但是可以使用构造器注入

     private List<String> noneSecurityList;
    
        //使用构造器注入,不能使用setget不然注不进去
        //因为这个filter在spring容器加载的时候就加载了。
        public RestPreAuthenticatedProcessingFilter(List<String> noneSecurityList) {
            this.noneSecurityList = noneSecurityList;
        }
    

    security配置类里的filter同样需要传入参数

    8.2、开始判断过来的请求在不在list里,我们可以使用spring的AntPathMatcher工具类帮我们匹配,它很好的封装了特殊字符的匹配

    //spring路径匹配器
    private AntPathMatcher matcher = new AntPathMatcher();
    
    /**
         * Author ljs
         * Description 校验是否无需权限的uri
         * Date 2018/9/30 16:59
         **/
        private boolean isNoneSecurity(String uri) {
            boolean result = false;
            if(this.noneSecurityList!=null){
                for(String pattern:this.noneSecurityList){
                    if(matcher.match(pattern,uri)){
                        result = true;
                        break;
                    }
                }
            }
            return result;
        }
    

    8.3、开始三种处理,filter就是判断url类型,并且提取信息封装到我们自定义的token里,然后发送到provider

     /**提取用户提交的信息,然后交给provider做校验,校验不通过进入entrypoint做异常处理**/
        @Override
        protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(1);
    
    
            /**第一种情况:无需拦截的请求**/
            if (isNoneSecurity(request.getRequestURL().toString()) || "OPTIONS".equals(request.getMethod())) {
                GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_SOMEONE");
                authorities.add(authority);
                //无需权限的url直接发放token走Provider授权
                return new RestAuthenticationToken(authorities);
            }
    
    
            /**第二种情况:需要拦截的请求**/
            String version = request.getHeader(Constants.REQUEST_VERSION_KEY);
            String token = request.getHeader(Constants.REQUEST_TOKEN_KEY);
    
    
            if (version == null) {
                request.setAttribute("header-error", 400);
            }
    
            /**校验token,如果header-error==null说明version是有值的**/
            if (request.getAttribute("header-error") == null) {
                try {
                    if (token != null && !token.trim().isEmpty()) {
                        UserElement ue = redis.getUserByToken(token);
    
                        if (ue instanceof UserElement) {
                            //检查到token说明用户已经登录 授权给用户BIKE_CLIENT角色 允许访问
                            GrantedAuthority authority = new SimpleGrantedAuthority("BIKE_CLIENT");
                            authorities.add(authority);
                            RestAuthenticationToken authToken = new RestAuthenticationToken(authorities);
                            authToken.setUser(ue);
                            return authToken;
                        }
                    } else {
                        log.warn("Got no token from request header");
                        //token不存在 告诉移动端 登录
                        request.setAttribute("header-error", 401);
                    }
                } catch (Exception e) {
                    log.error("Fail to authenticate user", e);
                }
    
            }
    
            /**第三种情况:给400和401一个角色,反正不能返回null,一定得返回一个token**/
            if (request.getAttribute("header-error") != null) {
                //请求头有错误  随便给个角色 让逻辑继续
                GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_NONE");
                authorities.add(authority);
            }
            RestAuthenticationToken authToken = new RestAuthenticationToken(authorities);
            return authToken;
        }
    

    8.4、provider,因为我filter传递的是自定义的token对象,所以都需要判断一下

    /**
         * Author ljs
         * Description 校验filter传递过来的对象,校验成功返回true
         * Date 2018/10/1 13:06
         **/
        @Override
        public boolean supports(Class<?> authentication) {
            return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication) || RestAuthenticationToken.class.isAssignableFrom(authentication);
        }
    

    8.5、开始授权,role_none抛出一个自定义的异常就行

    /**
         * Author ljs
         * Description 对符合要求的合法token授权
         * Date 2018/10/1 13:06
         **/
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            if (authentication instanceof PreAuthenticatedAuthenticationToken) {
                PreAuthenticatedAuthenticationToken preAuth = (PreAuthenticatedAuthenticationToken) authentication;
                RestAuthenticationToken sysAuth = (RestAuthenticationToken) preAuth.getPrincipal();
                //开始判断用户角色
                if (sysAuth.getAuthorities() != null && sysAuth.getAuthorities().size() > 0) {
                    GrantedAuthority authority = sysAuth.getAuthorities().iterator().next();
                    if ("BIKE_CLIENT".equals(authority.getAuthority())) {
                        return sysAuth;
                    } else if ("ROLE_SOMEONE".equals(authority.getAuthority())) {
                        return sysAuth;
                    }
                }
            } else if (authentication instanceof RestAuthenticationToken) {
                RestAuthenticationToken sysAuth = (RestAuthenticationToken) authentication;
                if (sysAuth.getAuthorities() != null && sysAuth.getAuthorities().size() > 0) {
                    GrantedAuthority gauth = sysAuth.getAuthorities().iterator().next();
                    if ("BIKE_CLIENT".equals(gauth.getAuthority())) {
                        return sysAuth;
                    } else if ("ROLE_SOMEONE".equals(gauth.getAuthority())) {
                        return sysAuth;
                    }
                }
            }
    
            throw new BadCredentialException("unknown.error");
        }
    

    8.6、entrypoint

    @Slf4j
    public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            ApiResult result = new ApiResult();
            //检查头部错误
            if (request.getAttribute("header-error") != null) {
                result.setCode(408);
                result.setMessage("请升级至app最新版本");
            } else {
                result.setCode(401);
                result.setMessage("请您登录");
            }
    
            try {
                //设置跨域请求 请求结果json刷到响应里
                response.setHeader("Access-Control-Allow-Origin", "*");
                response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEADER");
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, user-token, Content-Type, Accept, version, type, platform");
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.getWriter().write(JSON.toJSONString(result));
                response.flushBuffer();
            } catch (Exception er) {
                log.error("Fail to send 401 response {}", er.getMessage());
            }
    
        }
    }
    

    security配置类

     //当我们设置了跨域之后,移动端会先发一个options请求来探测一下
        //确认你允不允许我跨域,都支持跨域哪些方法,所以我们需要不能拦截options方法的请求
        @Override
        public void configure(WebSecurity web) throws Exception {
            //忽略 OPTIONS 方法的请求
            web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
            //放过swagger
        }
    

    debug测试:
    先是不带version的modifyNickName


    image.png

    带version


    image.png

    但是测试login的时候,它并没有走第一种情况:无需拦截的请求,原来是
    uri写错成url,urlhttp://localhost:8888/user/login,uri就只有user/login,改过来就行了。

    isNoneSecurity(request.getRequestURL().toString())
    

    总结:filter->manager->provider 出错就到entrypoint
    把它背了。。。以后所有移动端套用就行了。

    相关文章

      网友评论

        本文标题:单车第五天

        本文链接:https://www.haomeiwen.com/subject/kpgcoftx.html